@indigoai-us/hq-cli 5.30.0 → 5.32.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/files-browse.js +40 -28
- package/dist/commands/group-grants.d.ts +76 -0
- package/dist/commands/group-grants.js +296 -0
- package/dist/commands/pack-install.d.ts +46 -1
- package/dist/commands/pack-install.js +96 -17
- package/dist/commands/packs.d.ts +22 -0
- package/dist/commands/packs.js +406 -0
- package/dist/commands/pkg-list.js +31 -5
- package/dist/index.js +12 -2
- package/dist/utils/pack-contributions.d.ts +87 -0
- package/dist/utils/pack-contributions.js +239 -0
- package/package.json +1 -1
- package/src/commands/files-browse.test.ts +49 -0
- package/src/commands/files-browse.ts +38 -26
- package/src/commands/group-grants.test.ts +291 -0
- package/src/commands/group-grants.ts +452 -0
- package/src/commands/pack-install.ts +129 -21
- package/src/commands/packs.ts +524 -0
- package/src/commands/pkg-list.ts +34 -3
- package/src/index.ts +12 -0
- package/src/utils/pack-contributions.test.ts +208 -0
- package/src/utils/pack-contributions.ts +303 -0
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* methods and `grantPathToPrefix` from hq-cloud.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
!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]="
|
|
37
|
+
!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]="cad165f9-6445-5434-969b-e0705274aa56")}catch(e){}}();
|
|
38
38
|
import chalk from "chalk";
|
|
39
39
|
import * as fs from "node:fs";
|
|
40
40
|
import * as path from "node:path";
|
|
@@ -559,9 +559,12 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
559
559
|
"bucket-relative (omit it to list the vault root). Mutually exclusive " +
|
|
560
560
|
"with --company.")
|
|
561
561
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
562
|
-
.action(async (pathArg, options) => {
|
|
562
|
+
.action(async (pathArg, options, command) => {
|
|
563
563
|
try {
|
|
564
|
-
|
|
564
|
+
// `--company` is declared on the parent `files` group, so commander
|
|
565
|
+
// binds it there — read merged opts to see it from the subcommand.
|
|
566
|
+
const company = command.optsWithGlobals().company;
|
|
567
|
+
if (options.personal && company) {
|
|
565
568
|
throw new Error("--personal and --company are mutually exclusive. Pick one.");
|
|
566
569
|
}
|
|
567
570
|
const accessToken = await ensureCognitoToken();
|
|
@@ -594,12 +597,12 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
594
597
|
"browse your personal vault.");
|
|
595
598
|
}
|
|
596
599
|
// Resolve slug — CLI flag wins, otherwise parse from path arg.
|
|
597
|
-
const slug =
|
|
600
|
+
const slug = company ?? parseCompanySlugFromPath(pathArg);
|
|
598
601
|
// If the user passed `--company` AND the path doesn't begin with
|
|
599
602
|
// companies/<that-slug>/, refuse — we'd otherwise vend creds for
|
|
600
603
|
// one company and list keys from another tree, which never makes
|
|
601
604
|
// sense (defense in depth against operator typos).
|
|
602
|
-
if (
|
|
605
|
+
if (company !== undefined) {
|
|
603
606
|
const fromPath = (() => {
|
|
604
607
|
try {
|
|
605
608
|
return parseCompanySlugFromPath(pathArg);
|
|
@@ -608,8 +611,8 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
608
611
|
return undefined;
|
|
609
612
|
}
|
|
610
613
|
})();
|
|
611
|
-
if (fromPath && fromPath !==
|
|
612
|
-
throw new Error(`--company '${
|
|
614
|
+
if (fromPath && fromPath !== company) {
|
|
615
|
+
throw new Error(`--company '${company}' disagrees with path slug '${fromPath}'.`);
|
|
613
616
|
}
|
|
614
617
|
}
|
|
615
618
|
// Confirm the slug resolves to a known membership — same pattern
|
|
@@ -644,9 +647,11 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
644
647
|
.option("--personal", "Read from the caller's canonical personal vault. <path> is treated as " +
|
|
645
648
|
"bucket-relative. Mutually exclusive with --company.")
|
|
646
649
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
647
|
-
.action(async (keyArg, options) => {
|
|
650
|
+
.action(async (keyArg, options, command) => {
|
|
648
651
|
try {
|
|
649
|
-
|
|
652
|
+
// `--company` is bound on the parent `files` group — read merged opts.
|
|
653
|
+
const company = command.optsWithGlobals().company;
|
|
654
|
+
if (options.personal && company) {
|
|
650
655
|
throw new Error("--personal and --company are mutually exclusive. Pick one.");
|
|
651
656
|
}
|
|
652
657
|
const accessToken = await ensureCognitoToken();
|
|
@@ -673,8 +678,8 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
673
678
|
}
|
|
674
679
|
return;
|
|
675
680
|
}
|
|
676
|
-
const slug =
|
|
677
|
-
if (
|
|
681
|
+
const slug = company ?? parseCompanySlugFromPath(keyArg);
|
|
682
|
+
if (company !== undefined) {
|
|
678
683
|
const fromPath = (() => {
|
|
679
684
|
try {
|
|
680
685
|
return parseCompanySlugFromPath(keyArg);
|
|
@@ -683,8 +688,8 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
683
688
|
return undefined;
|
|
684
689
|
}
|
|
685
690
|
})();
|
|
686
|
-
if (fromPath && fromPath !==
|
|
687
|
-
throw new Error(`--company '${
|
|
691
|
+
if (fromPath && fromPath !== company) {
|
|
692
|
+
throw new Error(`--company '${company}' disagrees with path slug '${fromPath}'.`);
|
|
688
693
|
}
|
|
689
694
|
}
|
|
690
695
|
await getCompanyUid(accessToken, slug);
|
|
@@ -710,21 +715,23 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
710
715
|
.command("shared-with-me")
|
|
711
716
|
.description("List the files/prefixes explicitly shared with you. Omit --company to roll up across every company you're a member of. Pure read — no download, no credentials vended. Owner/admin role-bypass access is NOT listed (only explicit grants).")
|
|
712
717
|
.option("--company <slug>", "Scope to a single company (defaults to a cross-company roll-up).")
|
|
713
|
-
.action(async (options) => {
|
|
718
|
+
.action(async (options, command) => {
|
|
714
719
|
try {
|
|
720
|
+
// `--company` is bound on the parent `files` group — read merged opts.
|
|
721
|
+
const company = command.optsWithGlobals().company;
|
|
715
722
|
const accessToken = await ensureCognitoToken();
|
|
716
723
|
const vaultConfig = buildVaultConfig(accessToken);
|
|
717
724
|
const client = new VaultClient(vaultConfig);
|
|
718
725
|
let companyUid;
|
|
719
|
-
if (
|
|
726
|
+
if (company) {
|
|
720
727
|
// Confirm membership + resolve UID, same early-failure pattern as
|
|
721
728
|
// browse/cat. Roll-up mode skips this and fans out internally.
|
|
722
|
-
companyUid = await getCompanyUid(accessToken,
|
|
729
|
+
companyUid = await getCompanyUid(accessToken, company);
|
|
723
730
|
}
|
|
724
731
|
const rows = await runSharedWithMe({
|
|
725
732
|
vaultClient: client,
|
|
726
733
|
companyUid,
|
|
727
|
-
companySlug:
|
|
734
|
+
companySlug: company,
|
|
728
735
|
});
|
|
729
736
|
console.log(formatSharedWithMeTable(rows));
|
|
730
737
|
}
|
|
@@ -738,9 +745,12 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
738
745
|
.description("Search vault object keys (case-insensitive path/name match) under a company without downloading. Requires --company (or --personal). Content search is not supported in v1.")
|
|
739
746
|
.option("--company <slug>", "Company slug to search.")
|
|
740
747
|
.option("--personal", "Search the caller's canonical personal vault. Mutually exclusive with --company.")
|
|
741
|
-
.action(async (query, options) => {
|
|
748
|
+
.action(async (query, options, command) => {
|
|
742
749
|
try {
|
|
743
|
-
|
|
750
|
+
// `--company` is declared on the parent `files` group too, so commander
|
|
751
|
+
// binds it there; read the merged (global+local) opts to see it.
|
|
752
|
+
const company = command.optsWithGlobals().company;
|
|
753
|
+
if (options.personal && company) {
|
|
744
754
|
throw new Error("--personal and --company are mutually exclusive. Pick one.");
|
|
745
755
|
}
|
|
746
756
|
const accessToken = await ensureCognitoToken();
|
|
@@ -763,13 +773,13 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
763
773
|
console.log(formatBrowseTable(rows));
|
|
764
774
|
return;
|
|
765
775
|
}
|
|
766
|
-
if (!
|
|
776
|
+
if (!company) {
|
|
767
777
|
throw new Error("search: --company <slug> is required (or --personal to search your personal vault).");
|
|
768
778
|
}
|
|
769
|
-
await getCompanyUid(accessToken,
|
|
779
|
+
await getCompanyUid(accessToken, company);
|
|
770
780
|
const rows = await runSearch({
|
|
771
781
|
query,
|
|
772
|
-
companySlug:
|
|
782
|
+
companySlug: company,
|
|
773
783
|
vaultClient: client,
|
|
774
784
|
s3Factory: defaultS3Factory,
|
|
775
785
|
region: DEFAULT_COGNITO.region,
|
|
@@ -787,12 +797,14 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
787
797
|
.option("--into <dir>", "Write into this directory instead of the in-place companies/<slug>/ location. No pin is registered.")
|
|
788
798
|
.option("--company <slug>", "Company slug (defaults to the slug parsed from <path>).")
|
|
789
799
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
790
|
-
.action(async (pathArg, options) => {
|
|
800
|
+
.action(async (pathArg, options, command) => {
|
|
791
801
|
try {
|
|
792
802
|
const accessToken = await ensureCognitoToken();
|
|
793
803
|
const client = new VaultClient(buildVaultConfig(accessToken));
|
|
794
|
-
|
|
795
|
-
|
|
804
|
+
// `--company` is bound on the parent `files` group — read merged opts.
|
|
805
|
+
const companyOpt = command.optsWithGlobals().company;
|
|
806
|
+
const slug = companyOpt ?? parseCompanySlugFromPath(pathArg);
|
|
807
|
+
if (companyOpt !== undefined) {
|
|
796
808
|
const fromPath = (() => {
|
|
797
809
|
try {
|
|
798
810
|
return parseCompanySlugFromPath(pathArg);
|
|
@@ -801,8 +813,8 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
801
813
|
return undefined;
|
|
802
814
|
}
|
|
803
815
|
})();
|
|
804
|
-
if (fromPath && fromPath !==
|
|
805
|
-
throw new Error(`--company '${
|
|
816
|
+
if (fromPath && fromPath !== companyOpt) {
|
|
817
|
+
throw new Error(`--company '${companyOpt}' disagrees with path slug '${fromPath}'.`);
|
|
806
818
|
}
|
|
807
819
|
}
|
|
808
820
|
await getCompanyUid(accessToken, slug);
|
|
@@ -827,4 +839,4 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
827
839
|
});
|
|
828
840
|
}
|
|
829
841
|
//# sourceMappingURL=files-browse.js.map
|
|
830
|
-
//# debugId=
|
|
842
|
+
//# debugId=cad165f9-6445-5434-969b-e0705274aa56
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `hq group-grants` — manage cross-company group access grants.
|
|
4
|
+
*
|
|
5
|
+
* A *group grant* lets a group defined in a SOURCE company be granted a role
|
|
6
|
+
* on a TARGET company. The hq-pro API authorizes the operation against the
|
|
7
|
+
* TARGET company (the caller must be able to invite into it). The CLI is a
|
|
8
|
+
* thin client — it passes the right params and surfaces the server's 403 as an
|
|
9
|
+
* actionable cross-tenant permission error. NO authorization is reimplemented
|
|
10
|
+
* here.
|
|
11
|
+
*
|
|
12
|
+
* Backend contract (hq-pro feature/agency-cross-company-group-grants):
|
|
13
|
+
* - POST /group-grants { groupId, sourceCompanyUid, targetCompanyUid, role }
|
|
14
|
+
* - POST /group-grants/revoke { groupId, sourceCompanyUid, targetCompanyUid }
|
|
15
|
+
* - GET /group-grants/outbound?sourceCompanyUid&groupId
|
|
16
|
+
* - GET /group-grants/inbound?companyUid
|
|
17
|
+
*
|
|
18
|
+
* All require a Cognito idToken bearer; 403 (code FORBIDDEN) on authz failure.
|
|
19
|
+
*/
|
|
20
|
+
export declare const VALID_GRANT_ROLES: Set<string>;
|
|
21
|
+
export type GrantRole = "owner" | "admin" | "member" | "guest";
|
|
22
|
+
export interface GroupGrant {
|
|
23
|
+
groupId: string;
|
|
24
|
+
sourceCompanyUid: string;
|
|
25
|
+
targetCompanyUid: string;
|
|
26
|
+
role: string;
|
|
27
|
+
grantedBy?: string;
|
|
28
|
+
grantedAt?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface GrantGroupOptions {
|
|
31
|
+
groupId: string;
|
|
32
|
+
sourceCompanyUid: string;
|
|
33
|
+
targetCompanyUid: string;
|
|
34
|
+
role: string;
|
|
35
|
+
token: string;
|
|
36
|
+
}
|
|
37
|
+
export interface RevokeGroupGrantOptions {
|
|
38
|
+
groupId: string;
|
|
39
|
+
sourceCompanyUid: string;
|
|
40
|
+
targetCompanyUid: string;
|
|
41
|
+
token: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Typed HTTP error mirroring members.ts's `InviteHttpError`. Carries the
|
|
45
|
+
* server status + optional error `code` (e.g. `FORBIDDEN`) so the command
|
|
46
|
+
* layer can render an actionable message and exit non-zero.
|
|
47
|
+
*/
|
|
48
|
+
export declare class GrantHttpError extends Error {
|
|
49
|
+
status: number;
|
|
50
|
+
code?: string | undefined;
|
|
51
|
+
constructor(status: number, message: string, code?: string | undefined);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Map a `GrantHttpError` onto an actionable, human-readable message. The 403
|
|
55
|
+
* branch is the story's key cross-tenant case: authorization is enforced
|
|
56
|
+
* against the TARGET company, so the message names the target the caller
|
|
57
|
+
* lacks rights on.
|
|
58
|
+
*/
|
|
59
|
+
export declare function formatGrantHttpError(status: number, fallback: string, ctx?: {
|
|
60
|
+
targetCompany?: string;
|
|
61
|
+
code?: string;
|
|
62
|
+
}): string;
|
|
63
|
+
/**
|
|
64
|
+
* POST /group-grants — grant a group's access to a target company at a role.
|
|
65
|
+
* Pure-ish: validates inputs, then calls the API client. Throws
|
|
66
|
+
* `GrantHttpError` on non-2xx so the command layer surfaces it.
|
|
67
|
+
*/
|
|
68
|
+
export declare function grantGroup(options: GrantGroupOptions): Promise<GroupGrant>;
|
|
69
|
+
/** POST /group-grants/revoke — remove a group's grant on a target company. */
|
|
70
|
+
export declare function revokeGroupGrant(options: RevokeGroupGrantOptions): Promise<void>;
|
|
71
|
+
/** GET /group-grants/outbound — grants a source company's group(s) hold. */
|
|
72
|
+
export declare function listOutboundGrants(token: string, sourceCompanyUid: string, groupId?: string): Promise<GroupGrant[]>;
|
|
73
|
+
/** GET /group-grants/inbound — grants other companies' groups hold on us. */
|
|
74
|
+
export declare function listInboundGrants(token: string, companyUid: string): Promise<GroupGrant[]>;
|
|
75
|
+
export declare function registerGroupGrantsCommand(program: Command): void;
|
|
76
|
+
//# sourceMappingURL=group-grants.d.ts.map
|
|
@@ -0,0 +1,296 @@
|
|
|
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]="eff1a01a-1996-5c23-aaf4-084319d454fc")}catch(e){}}();
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
|
+
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
6
|
+
import { GROUP_ID_PATTERN } from "./_patterns.js";
|
|
7
|
+
/**
|
|
8
|
+
* `hq group-grants` — manage cross-company group access grants.
|
|
9
|
+
*
|
|
10
|
+
* A *group grant* lets a group defined in a SOURCE company be granted a role
|
|
11
|
+
* on a TARGET company. The hq-pro API authorizes the operation against the
|
|
12
|
+
* TARGET company (the caller must be able to invite into it). The CLI is a
|
|
13
|
+
* thin client — it passes the right params and surfaces the server's 403 as an
|
|
14
|
+
* actionable cross-tenant permission error. NO authorization is reimplemented
|
|
15
|
+
* here.
|
|
16
|
+
*
|
|
17
|
+
* Backend contract (hq-pro feature/agency-cross-company-group-grants):
|
|
18
|
+
* - POST /group-grants { groupId, sourceCompanyUid, targetCompanyUid, role }
|
|
19
|
+
* - POST /group-grants/revoke { groupId, sourceCompanyUid, targetCompanyUid }
|
|
20
|
+
* - GET /group-grants/outbound?sourceCompanyUid&groupId
|
|
21
|
+
* - GET /group-grants/inbound?companyUid
|
|
22
|
+
*
|
|
23
|
+
* All require a Cognito idToken bearer; 403 (code FORBIDDEN) on authz failure.
|
|
24
|
+
*/
|
|
25
|
+
export const VALID_GRANT_ROLES = new Set(["owner", "admin", "member", "guest"]);
|
|
26
|
+
/**
|
|
27
|
+
* Typed HTTP error mirroring members.ts's `InviteHttpError`. Carries the
|
|
28
|
+
* server status + optional error `code` (e.g. `FORBIDDEN`) so the command
|
|
29
|
+
* layer can render an actionable message and exit non-zero.
|
|
30
|
+
*/
|
|
31
|
+
export class GrantHttpError extends Error {
|
|
32
|
+
status;
|
|
33
|
+
code;
|
|
34
|
+
constructor(status, message, code) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.name = "GrantHttpError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Map a `GrantHttpError` onto an actionable, human-readable message. The 403
|
|
43
|
+
* branch is the story's key cross-tenant case: authorization is enforced
|
|
44
|
+
* against the TARGET company, so the message names the target the caller
|
|
45
|
+
* lacks rights on.
|
|
46
|
+
*/
|
|
47
|
+
export function formatGrantHttpError(status, fallback, ctx = {}) {
|
|
48
|
+
if (status === 401)
|
|
49
|
+
return "Not authenticated — please run `hq login`";
|
|
50
|
+
if (status === 403) {
|
|
51
|
+
const target = ctx.targetCompany ? ` '${ctx.targetCompany}'` : "";
|
|
52
|
+
return (`Permission denied: you must be an owner or admin of the target company${target} to grant a group into it. ` +
|
|
53
|
+
"The grant is authorized against the target company — ask an owner/admin there, or have them enable adminCanInvite.");
|
|
54
|
+
}
|
|
55
|
+
if (status === 404) {
|
|
56
|
+
return `Not found: ${fallback}. Check the group id and that both companies exist.`;
|
|
57
|
+
}
|
|
58
|
+
if (status === 409) {
|
|
59
|
+
return `That grant already exists: ${fallback}`;
|
|
60
|
+
}
|
|
61
|
+
if (status >= 500)
|
|
62
|
+
return `Server error: ${fallback}`;
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
function shortDate(iso) {
|
|
66
|
+
return iso.slice(0, 10);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* POST /group-grants — grant a group's access to a target company at a role.
|
|
70
|
+
* Pure-ish: validates inputs, then calls the API client. Throws
|
|
71
|
+
* `GrantHttpError` on non-2xx so the command layer surfaces it.
|
|
72
|
+
*/
|
|
73
|
+
export async function grantGroup(options) {
|
|
74
|
+
if (!GROUP_ID_PATTERN.test(options.groupId)) {
|
|
75
|
+
throw new Error(`Invalid group id '${options.groupId}': must match grp_<alphanumeric, underscore, hyphen>`);
|
|
76
|
+
}
|
|
77
|
+
if (!VALID_GRANT_ROLES.has(options.role)) {
|
|
78
|
+
throw new Error(`Invalid role '${options.role}': must be one of owner, admin, member, guest`);
|
|
79
|
+
}
|
|
80
|
+
if (options.sourceCompanyUid === options.targetCompanyUid) {
|
|
81
|
+
throw new Error("Source and target companies are the same — a group grant must cross company boundaries.");
|
|
82
|
+
}
|
|
83
|
+
const res = await vaultApiFetch({
|
|
84
|
+
token: options.token,
|
|
85
|
+
path: "/group-grants",
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: {
|
|
88
|
+
groupId: options.groupId,
|
|
89
|
+
sourceCompanyUid: options.sourceCompanyUid,
|
|
90
|
+
targetCompanyUid: options.targetCompanyUid,
|
|
91
|
+
role: options.role,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const err = (await res.json().catch(() => ({})));
|
|
96
|
+
throw new GrantHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
97
|
+
}
|
|
98
|
+
const data = (await res.json());
|
|
99
|
+
return (data.grant ?? {
|
|
100
|
+
groupId: options.groupId,
|
|
101
|
+
sourceCompanyUid: options.sourceCompanyUid,
|
|
102
|
+
targetCompanyUid: options.targetCompanyUid,
|
|
103
|
+
role: options.role,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** POST /group-grants/revoke — remove a group's grant on a target company. */
|
|
107
|
+
export async function revokeGroupGrant(options) {
|
|
108
|
+
if (!GROUP_ID_PATTERN.test(options.groupId)) {
|
|
109
|
+
throw new Error(`Invalid group id '${options.groupId}': must match grp_<alphanumeric, underscore, hyphen>`);
|
|
110
|
+
}
|
|
111
|
+
const res = await vaultApiFetch({
|
|
112
|
+
token: options.token,
|
|
113
|
+
path: "/group-grants/revoke",
|
|
114
|
+
method: "POST",
|
|
115
|
+
body: {
|
|
116
|
+
groupId: options.groupId,
|
|
117
|
+
sourceCompanyUid: options.sourceCompanyUid,
|
|
118
|
+
targetCompanyUid: options.targetCompanyUid,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
const err = (await res.json().catch(() => ({})));
|
|
123
|
+
throw new GrantHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** GET /group-grants/outbound — grants a source company's group(s) hold. */
|
|
127
|
+
export async function listOutboundGrants(token, sourceCompanyUid, groupId) {
|
|
128
|
+
const query = { sourceCompanyUid };
|
|
129
|
+
if (groupId)
|
|
130
|
+
query.groupId = groupId;
|
|
131
|
+
const res = await vaultApiFetch({
|
|
132
|
+
token,
|
|
133
|
+
path: "/group-grants/outbound",
|
|
134
|
+
query,
|
|
135
|
+
});
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const err = (await res.json().catch(() => ({})));
|
|
138
|
+
throw new GrantHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
139
|
+
}
|
|
140
|
+
const data = (await res.json());
|
|
141
|
+
return data.grants ?? [];
|
|
142
|
+
}
|
|
143
|
+
/** GET /group-grants/inbound — grants other companies' groups hold on us. */
|
|
144
|
+
export async function listInboundGrants(token, companyUid) {
|
|
145
|
+
const res = await vaultApiFetch({
|
|
146
|
+
token,
|
|
147
|
+
path: "/group-grants/inbound",
|
|
148
|
+
query: { companyUid },
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
const err = (await res.json().catch(() => ({})));
|
|
152
|
+
throw new GrantHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
153
|
+
}
|
|
154
|
+
const data = (await res.json());
|
|
155
|
+
return data.grants ?? [];
|
|
156
|
+
}
|
|
157
|
+
function printGrantsTable(grants) {
|
|
158
|
+
const idW = Math.max(8, ...grants.map((g) => g.groupId.length));
|
|
159
|
+
const srcW = Math.max(6, ...grants.map((g) => g.sourceCompanyUid.length));
|
|
160
|
+
const tgtW = Math.max(6, ...grants.map((g) => g.targetCompanyUid.length));
|
|
161
|
+
const roleW = Math.max(4, ...grants.map((g) => g.role.length));
|
|
162
|
+
console.log(chalk.bold([
|
|
163
|
+
"GROUP_ID".padEnd(idW),
|
|
164
|
+
"SOURCE".padEnd(srcW),
|
|
165
|
+
"TARGET".padEnd(tgtW),
|
|
166
|
+
"ROLE".padEnd(roleW),
|
|
167
|
+
"GRANTED_AT",
|
|
168
|
+
].join(" ")));
|
|
169
|
+
for (const g of grants) {
|
|
170
|
+
console.log([
|
|
171
|
+
g.groupId.padEnd(idW),
|
|
172
|
+
g.sourceCompanyUid.padEnd(srcW),
|
|
173
|
+
g.targetCompanyUid.padEnd(tgtW),
|
|
174
|
+
g.role.padEnd(roleW),
|
|
175
|
+
g.grantedAt ? shortDate(g.grantedAt) : "",
|
|
176
|
+
].join(" "));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export function registerGroupGrantsCommand(program) {
|
|
180
|
+
const grants = program
|
|
181
|
+
.command("group-grants")
|
|
182
|
+
.description("Grant a group's access to another company (cross-company), and revoke or inspect those grants")
|
|
183
|
+
.option("--company <slug>", "Source company slug (the company that owns the group; resolves to sourceCompanyUid)");
|
|
184
|
+
grants
|
|
185
|
+
.command("grant <groupId> <targetCompany>")
|
|
186
|
+
.description("Grant a group (from the source company) access to <targetCompany> at a role")
|
|
187
|
+
.option("--role <role>", "Role the group receives on the target company: owner, admin, member, or guest", "member")
|
|
188
|
+
.action(async (groupId, targetCompany, opts) => {
|
|
189
|
+
try {
|
|
190
|
+
const token = await ensureCognitoToken();
|
|
191
|
+
const sourceSlug = grants.opts().company;
|
|
192
|
+
const sourceCompanyUid = await getCompanyUid(token, sourceSlug);
|
|
193
|
+
const targetCompanyUid = await getCompanyUid(token, targetCompany);
|
|
194
|
+
const grant = await grantGroup({
|
|
195
|
+
groupId,
|
|
196
|
+
sourceCompanyUid,
|
|
197
|
+
targetCompanyUid,
|
|
198
|
+
role: opts.role,
|
|
199
|
+
token,
|
|
200
|
+
});
|
|
201
|
+
console.log(chalk.green(`Granted group '${grant.groupId}' access to '${targetCompany}' as ${grant.role}`));
|
|
202
|
+
console.log(chalk.dim(` source: ${grant.sourceCompanyUid} → target: ${grant.targetCompanyUid}`));
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
if (err instanceof GrantHttpError) {
|
|
206
|
+
console.error(chalk.red(formatGrantHttpError(err.status, err.message, {
|
|
207
|
+
targetCompany,
|
|
208
|
+
code: err.code,
|
|
209
|
+
})));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
grants
|
|
217
|
+
.command("revoke <groupId> <targetCompany>")
|
|
218
|
+
.description("Revoke a group's grant on <targetCompany>")
|
|
219
|
+
.action(async (groupId, targetCompany) => {
|
|
220
|
+
try {
|
|
221
|
+
const token = await ensureCognitoToken();
|
|
222
|
+
const sourceSlug = grants.opts().company;
|
|
223
|
+
const sourceCompanyUid = await getCompanyUid(token, sourceSlug);
|
|
224
|
+
const targetCompanyUid = await getCompanyUid(token, targetCompany);
|
|
225
|
+
await revokeGroupGrant({
|
|
226
|
+
groupId,
|
|
227
|
+
sourceCompanyUid,
|
|
228
|
+
targetCompanyUid,
|
|
229
|
+
token,
|
|
230
|
+
});
|
|
231
|
+
console.log(chalk.green(`Revoked group '${groupId}' grant on '${targetCompany}'`));
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err instanceof GrantHttpError) {
|
|
235
|
+
console.error(chalk.red(formatGrantHttpError(err.status, err.message, {
|
|
236
|
+
targetCompany,
|
|
237
|
+
code: err.code,
|
|
238
|
+
})));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
grants
|
|
246
|
+
.command("outbound")
|
|
247
|
+
.description("List grants the source company's groups hold on other companies")
|
|
248
|
+
.option("--group <groupId>", "Filter to a single group id")
|
|
249
|
+
.action(async (opts) => {
|
|
250
|
+
try {
|
|
251
|
+
const token = await ensureCognitoToken();
|
|
252
|
+
const sourceSlug = grants.opts().company;
|
|
253
|
+
const sourceCompanyUid = await getCompanyUid(token, sourceSlug);
|
|
254
|
+
const list = await listOutboundGrants(token, sourceCompanyUid, opts.group);
|
|
255
|
+
if (list.length === 0) {
|
|
256
|
+
console.log(chalk.gray("No outbound group grants."));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
printGrantsTable(list);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
if (err instanceof GrantHttpError) {
|
|
263
|
+
console.error(chalk.red(formatGrantHttpError(err.status, err.message)));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
grants
|
|
271
|
+
.command("inbound")
|
|
272
|
+
.description("List grants other companies' groups hold on this company")
|
|
273
|
+
.action(async () => {
|
|
274
|
+
try {
|
|
275
|
+
const token = await ensureCognitoToken();
|
|
276
|
+
const companySlug = grants.opts().company;
|
|
277
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
278
|
+
const list = await listInboundGrants(token, companyUid);
|
|
279
|
+
if (list.length === 0) {
|
|
280
|
+
console.log(chalk.gray("No inbound group grants."));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
printGrantsTable(list);
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
if (err instanceof GrantHttpError) {
|
|
287
|
+
console.error(chalk.red(formatGrantHttpError(err.status, err.message)));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=group-grants.js.map
|
|
296
|
+
//# debugId=eff1a01a-1996-5c23-aaf4-084319d454fc
|
|
@@ -34,12 +34,44 @@
|
|
|
34
34
|
* from each pack's package.yaml; rationale lives in the layout-fix PR.)
|
|
35
35
|
*/
|
|
36
36
|
import type { PackManifest } from '../types.js';
|
|
37
|
+
export type Transport = 'npm' | 'git' | 'local';
|
|
38
|
+
export declare function classify(source: string): Transport;
|
|
39
|
+
/**
|
|
40
|
+
* Parse a git source's '#<...>' fragment into { url, subpath, ref }.
|
|
41
|
+
* Disambiguation: fragment containing '/' is a subpath (optionally with
|
|
42
|
+
* '@<ref>' suffix); fragment without '/' is a ref.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseGitFragment(source: string): {
|
|
45
|
+
url: string;
|
|
46
|
+
subpath?: string;
|
|
47
|
+
ref?: string;
|
|
48
|
+
};
|
|
37
49
|
/**
|
|
38
50
|
* sourceMatchesPackPattern — exported for the dispatcher in pkg-install.ts
|
|
39
51
|
* so it can decide whether to route to the new content-pack handler or fall
|
|
40
52
|
* back to the legacy registry flow.
|
|
41
53
|
*/
|
|
42
54
|
export declare function sourceMatchesPackPattern(source: string): boolean;
|
|
55
|
+
export interface LatestResult {
|
|
56
|
+
transport: Transport;
|
|
57
|
+
/** Identifier of the currently-installed pack (sha for git, version for npm). */
|
|
58
|
+
current?: string;
|
|
59
|
+
/** Latest available identifier from the remote. */
|
|
60
|
+
latest?: string;
|
|
61
|
+
/** true/false when a comparison was possible; null when undeterminable (local). */
|
|
62
|
+
updateAvailable: boolean | null;
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Probe whether a newer version of an already-installed pack is available,
|
|
67
|
+
* WITHOUT fetching or installing. Reuses the same git/npm primitives as the
|
|
68
|
+
* install path. Never throws — network/parse failures return
|
|
69
|
+
* `{ updateAvailable: null, error }` so callers (the menubar) stay resilient.
|
|
70
|
+
*
|
|
71
|
+
* @param source the stamped `source:` from the installed package.yaml
|
|
72
|
+
* @param installedVersion the installed pack's manifest `version` (npm compare)
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveLatest(source: string, installedVersion?: string): LatestResult;
|
|
43
75
|
/**
|
|
44
76
|
* Install the fetched payload to `<hqRoot>/core/packages/<pkg.name>/` (HQ
|
|
45
77
|
* v12+ layout). The HQ template (`hq-core` / `hq-core-staging`) ships
|
|
@@ -86,11 +118,24 @@ export declare function stampInstallSource(destDir: string, source: string): voi
|
|
|
86
118
|
* own scan.
|
|
87
119
|
*
|
|
88
120
|
* Exported for tests.
|
|
121
|
+
*
|
|
122
|
+
* `quiet` keeps the script's stdout off our stdout (it routes only stderr
|
|
123
|
+
* through, and sets HQ_SCAN_QUIET=1) so callers emitting machine-readable
|
|
124
|
+
* JSON — e.g. `hq packs uninstall --json` — produce clean output.
|
|
89
125
|
*/
|
|
90
|
-
export declare function runScanPackages(hqRoot: string
|
|
126
|
+
export declare function runScanPackages(hqRoot: string, opts?: {
|
|
127
|
+
quiet?: boolean;
|
|
128
|
+
}): void;
|
|
91
129
|
export interface InstallPackOptions {
|
|
92
130
|
allowHooks?: boolean;
|
|
93
131
|
followBranch?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Route this function's human output to stderr (and silence scan-packages
|
|
134
|
+
* stdout) so a caller emitting machine-readable JSON keeps stdout clean.
|
|
135
|
+
* The fetch tools are already quiet/stderr-only (`npm pack --silent`,
|
|
136
|
+
* `rsync -a`, `git clone` progress -> stderr), so this is sufficient.
|
|
137
|
+
*/
|
|
138
|
+
quiet?: boolean;
|
|
94
139
|
}
|
|
95
140
|
export declare function installPack(source: string, opts?: InstallPackOptions): Promise<void>;
|
|
96
141
|
//# sourceMappingURL=pack-install.d.ts.map
|