@indigoai-us/hq-cli 5.144.0 → 5.146.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 +19 -0
- package/dist/command-catalog.generated.d.ts +24 -6
- package/dist/command-catalog.generated.js +29 -6
- package/dist/commands/_patterns.d.ts +16 -0
- package/dist/commands/_patterns.js +20 -3
- package/dist/commands/files-browse.d.ts +21 -10
- package/dist/commands/files-browse.js +95 -32
- package/dist/commands/files.js +44 -13
- package/dist/commands/mesh.js +115 -1
- package/dist/commands/report.d.ts +13 -0
- package/dist/commands/report.js +35 -13
- package/dist/commands/sync-narrow.js +36 -5
- package/dist/commands/workers.js +5 -2
- package/dist/lib/mesh/live/daemon/credentials.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/credentials.js +141 -10
- package/dist/lib/mesh/live/daemon/doctor.js +4 -3
- package/dist/lib/mesh/live/daemon/presence.d.ts +15 -0
- package/dist/lib/mesh/live/daemon/presence.js +69 -1
- package/dist/lib/mesh/live/daemon/run.js +3 -1
- package/dist/lib/mesh/project-board.d.ts +14 -0
- package/dist/lib/mesh/project-board.js +24 -1
- package/dist/lib/work-context/bind-best.d.ts +2 -0
- package/dist/lib/work-context/bind-best.js +34 -1
- package/dist/lib/work-context/bind-calling-session.d.ts +26 -0
- package/dist/lib/work-context/bind-calling-session.js +63 -0
- package/dist/lib/work-context/prd-sync.d.ts +34 -0
- package/dist/lib/work-context/prd-sync.js +120 -0
- package/dist/lib/work-context/prompt-rebind.d.ts +21 -0
- package/dist/lib/work-context/prompt-rebind.js +76 -0
- package/dist/lib/work-context/reconcile.d.ts +2 -0
- package/dist/lib/work-context/reconcile.js +31 -0
- package/dist/lib/work-context/state.d.ts +2 -0
- package/dist/lib/work-context/state.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.146.0] — 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- The mesh daemon asks for presence in the device-default company and the companies with a session updated in the last day (at most five), and asks again within a minute when that set changes. `hq mesh doctor` says when a company was left out because presence is off or because one credential cannot cover every company.
|
|
10
|
+
- `hq mesh project ensure` closes a genesis thread only when that same call created it and register returns a different server `proj-` thread. An already-open project thread stays open on a re-run. The register route does not accept a client thread id.
|
|
11
|
+
- `hq mesh project ensure` on a new project closes the client genesis thread when register returns the server `proj-` thread, so the project has one open thread. The register route does not accept a client thread id.
|
|
12
|
+
- A session that registers or creates a project is bound to it. `hq mesh project ensure` and `hq mesh project set --create` bind the calling session when it is the same company and still `needs_project` or unresolved, and they leave a session that is already on a different project alone. The next prompt re-checks `needs_project` against the current catalog. `hq mesh context prd-sync --session <sid> --file <prd.json>` emits `task_status` when a bound session's story `passes` or `status` changes (`passes: true` is done, `in_progress` is in progress) and does not call the network when nothing changed.
|
|
13
|
+
- `hq report publish` now fills a report's missing company from its channel and keeps it across updates. Before, a report saved without a company was published under a personal access policy and the deploy refused it.
|
|
14
|
+
|
|
15
|
+
## [5.145.0] — 2026-09-23
|
|
16
|
+
|
|
17
|
+
- `hq files` now preserves a trailing-slash private-folder pattern such as
|
|
18
|
+
`foo/` rather than rewriting it to `foo/*`, identifies private folders in
|
|
19
|
+
ACL and discovery output, and explains the conflict with an existing shared
|
|
20
|
+
glob. Quote a shared glob such as `'foo/*'` when invoking it from a shell.
|
|
21
|
+
- `hq report` now finds reports on project channels. It listed only personal channels and treated only company-wide channels as valid, so every publish from a bot on a project channel failed with a generic deploy error. Channel lookup errors now say what went wrong.
|
|
22
|
+
|
|
5
23
|
## [5.144.0] — 2026-09-23
|
|
6
24
|
|
|
7
25
|
- Worker-profile models now use the same provider catalog validation as
|
|
@@ -50,6 +68,7 @@
|
|
|
50
68
|
### Fixed
|
|
51
69
|
|
|
52
70
|
- `hq lanes monitor-check` now recognizes a live Claude Monitor task launched through the CLI's shell/node process wrapper, so active lanes are not reported as uncovered.
|
|
71
|
+
|
|
53
72
|
- A session hint now binds to the best existing project. An exact Board is used as-is. An exact local PRD gets a Board through the same genesis-thread, view, then register path as `project ensure`, and is bound. A single clear match is bound with a recorded reason. When nothing matches, or two candidates are close, the session stays `needs_project` and the start hook lists the candidates. `hq mesh context bind-project` applies the answer. A flush HTTP 404 downgrades that session, holds its events, and logs one line instead of dead-lettering them. `hq mesh doctor` lists `needs_project` sessions.
|
|
54
73
|
|
|
55
74
|
## [5.139.0] — 2026-09-23
|
|
@@ -1642,7 +1642,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1642
1642
|
}];
|
|
1643
1643
|
}, {
|
|
1644
1644
|
readonly name: "files";
|
|
1645
|
-
readonly description: "Manage file access controls in HQ vault";
|
|
1645
|
+
readonly description: "Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children.";
|
|
1646
1646
|
readonly aliases: readonly [];
|
|
1647
1647
|
readonly hidden: false;
|
|
1648
1648
|
readonly usage: "[options] [command]";
|
|
@@ -1653,7 +1653,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1653
1653
|
}];
|
|
1654
1654
|
readonly subcommands: readonly [{
|
|
1655
1655
|
readonly name: "share";
|
|
1656
|
-
readonly 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.";
|
|
1656
|
+
readonly description: "Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*' so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.";
|
|
1657
1657
|
readonly aliases: readonly [];
|
|
1658
1658
|
readonly hidden: false;
|
|
1659
1659
|
readonly usage: "[options] [paths...]";
|
|
@@ -1687,7 +1687,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1687
1687
|
readonly subcommands: readonly [];
|
|
1688
1688
|
}, {
|
|
1689
1689
|
readonly name: "unshare";
|
|
1690
|
-
readonly description: "Remove a file access grant";
|
|
1690
|
+
readonly description: "Remove a file access grant. A trailing slash targets the private-folder row exactly.";
|
|
1691
1691
|
readonly aliases: readonly [];
|
|
1692
1692
|
readonly hidden: false;
|
|
1693
1693
|
readonly usage: "[options] <prefix>";
|
|
@@ -1703,7 +1703,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1703
1703
|
readonly subcommands: readonly [];
|
|
1704
1704
|
}, {
|
|
1705
1705
|
readonly name: "acl";
|
|
1706
|
-
readonly description: "Show the ACL (access control list) for a file prefix";
|
|
1706
|
+
readonly description: "Show the ACL (access control list) for a file prefix. foo/ is a private folder whose children are creator-only; the quoted glob 'foo/*' is shared.";
|
|
1707
1707
|
readonly aliases: readonly [];
|
|
1708
1708
|
readonly hidden: false;
|
|
1709
1709
|
readonly usage: "[options] <prefix>";
|
|
@@ -1797,7 +1797,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1797
1797
|
readonly subcommands: readonly [];
|
|
1798
1798
|
}, {
|
|
1799
1799
|
readonly name: "browse";
|
|
1800
|
-
readonly description: "List vault objects under [path] without syncing them locally. Uses the browse-vend path (role-bypass allowed). Pass --personal to browse the caller's personal vault; otherwise [path] must start with companies/<slug>/.";
|
|
1800
|
+
readonly description: "List vault objects under [path] without syncing them locally. Private-folder rows (foo/) are labelled and do not imply child access; their children are creator-only. Uses the browse-vend path (role-bypass allowed). Pass --personal to browse the caller's personal vault; otherwise [path] must start with companies/<slug>/.";
|
|
1801
1801
|
readonly aliases: readonly [];
|
|
1802
1802
|
readonly hidden: false;
|
|
1803
1803
|
readonly usage: "[options] [path]";
|
|
@@ -1844,7 +1844,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1844
1844
|
readonly subcommands: readonly [];
|
|
1845
1845
|
}, {
|
|
1846
1846
|
readonly name: "shared-with-me";
|
|
1847
|
-
readonly 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).";
|
|
1847
|
+
readonly description: "List the files/prefixes explicitly shared with you. Private folders (foo/) expose only the folder; children are creator-only. 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).";
|
|
1848
1848
|
readonly aliases: readonly [];
|
|
1849
1849
|
readonly hidden: false;
|
|
1850
1850
|
readonly usage: "[options]";
|
|
@@ -5882,6 +5882,24 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5882
5882
|
readonly description: "Print machine-readable JSON";
|
|
5883
5883
|
}];
|
|
5884
5884
|
readonly subcommands: readonly [];
|
|
5885
|
+
}, {
|
|
5886
|
+
readonly name: "prd-sync";
|
|
5887
|
+
readonly description: "Emit task_status for PRD story passes/status changes on a bound session";
|
|
5888
|
+
readonly aliases: readonly [];
|
|
5889
|
+
readonly hidden: false;
|
|
5890
|
+
readonly usage: "[options]";
|
|
5891
|
+
readonly arguments: readonly [];
|
|
5892
|
+
readonly options: readonly [{
|
|
5893
|
+
readonly flags: "--session <sid>";
|
|
5894
|
+
readonly description: "Canonical session id";
|
|
5895
|
+
}, {
|
|
5896
|
+
readonly flags: "--file <path>";
|
|
5897
|
+
readonly description: "prd.json written by the bound session";
|
|
5898
|
+
}, {
|
|
5899
|
+
readonly flags: "--json";
|
|
5900
|
+
readonly description: "Print machine-readable JSON";
|
|
5901
|
+
}];
|
|
5902
|
+
readonly subcommands: readonly [];
|
|
5885
5903
|
}, {
|
|
5886
5904
|
readonly name: "organize";
|
|
5887
5905
|
readonly description: "List or submit one-time project/task decisions (US-007B / US-005B)";
|
|
@@ -2093,7 +2093,7 @@ export const COMMAND_CATALOG = [
|
|
|
2093
2093
|
},
|
|
2094
2094
|
{
|
|
2095
2095
|
"name": "files",
|
|
2096
|
-
"description": "Manage file access controls in HQ vault",
|
|
2096
|
+
"description": "Manage file access controls in HQ vault. A trailing folder slash (foo/) is a private folder; the quoted glob 'foo/*' shares all children.",
|
|
2097
2097
|
"aliases": [],
|
|
2098
2098
|
"hidden": false,
|
|
2099
2099
|
"usage": "[options] [command]",
|
|
@@ -2107,7 +2107,7 @@ export const COMMAND_CATALOG = [
|
|
|
2107
2107
|
"subcommands": [
|
|
2108
2108
|
{
|
|
2109
2109
|
"name": "share",
|
|
2110
|
-
"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.",
|
|
2110
|
+
"description": "Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*' so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.",
|
|
2111
2111
|
"aliases": [],
|
|
2112
2112
|
"hidden": false,
|
|
2113
2113
|
"usage": "[options] [paths...]",
|
|
@@ -2152,7 +2152,7 @@ export const COMMAND_CATALOG = [
|
|
|
2152
2152
|
},
|
|
2153
2153
|
{
|
|
2154
2154
|
"name": "unshare",
|
|
2155
|
-
"description": "Remove a file access grant",
|
|
2155
|
+
"description": "Remove a file access grant. A trailing slash targets the private-folder row exactly.",
|
|
2156
2156
|
"aliases": [],
|
|
2157
2157
|
"hidden": false,
|
|
2158
2158
|
"usage": "[options] <prefix>",
|
|
@@ -2173,7 +2173,7 @@ export const COMMAND_CATALOG = [
|
|
|
2173
2173
|
},
|
|
2174
2174
|
{
|
|
2175
2175
|
"name": "acl",
|
|
2176
|
-
"description": "Show the ACL (access control list) for a file prefix",
|
|
2176
|
+
"description": "Show the ACL (access control list) for a file prefix. foo/ is a private folder whose children are creator-only; the quoted glob 'foo/*' is shared.",
|
|
2177
2177
|
"aliases": [],
|
|
2178
2178
|
"hidden": false,
|
|
2179
2179
|
"usage": "[options] <prefix>",
|
|
@@ -2296,7 +2296,7 @@ export const COMMAND_CATALOG = [
|
|
|
2296
2296
|
},
|
|
2297
2297
|
{
|
|
2298
2298
|
"name": "browse",
|
|
2299
|
-
"description": "List vault objects under [path] without syncing them locally. Uses the browse-vend path (role-bypass allowed). Pass --personal to browse the caller's personal vault; otherwise [path] must start with companies/<slug>/.",
|
|
2299
|
+
"description": "List vault objects under [path] without syncing them locally. Private-folder rows (foo/) are labelled and do not imply child access; their children are creator-only. Uses the browse-vend path (role-bypass allowed). Pass --personal to browse the caller's personal vault; otherwise [path] must start with companies/<slug>/.",
|
|
2300
2300
|
"aliases": [],
|
|
2301
2301
|
"hidden": false,
|
|
2302
2302
|
"usage": "[options] [path]",
|
|
@@ -2358,7 +2358,7 @@ export const COMMAND_CATALOG = [
|
|
|
2358
2358
|
},
|
|
2359
2359
|
{
|
|
2360
2360
|
"name": "shared-with-me",
|
|
2361
|
-
"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).",
|
|
2361
|
+
"description": "List the files/prefixes explicitly shared with you. Private folders (foo/) expose only the folder; children are creator-only. 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).",
|
|
2362
2362
|
"aliases": [],
|
|
2363
2363
|
"hidden": false,
|
|
2364
2364
|
"usage": "[options]",
|
|
@@ -7593,6 +7593,29 @@ export const COMMAND_CATALOG = [
|
|
|
7593
7593
|
],
|
|
7594
7594
|
"subcommands": []
|
|
7595
7595
|
},
|
|
7596
|
+
{
|
|
7597
|
+
"name": "prd-sync",
|
|
7598
|
+
"description": "Emit task_status for PRD story passes/status changes on a bound session",
|
|
7599
|
+
"aliases": [],
|
|
7600
|
+
"hidden": false,
|
|
7601
|
+
"usage": "[options]",
|
|
7602
|
+
"arguments": [],
|
|
7603
|
+
"options": [
|
|
7604
|
+
{
|
|
7605
|
+
"flags": "--session <sid>",
|
|
7606
|
+
"description": "Canonical session id"
|
|
7607
|
+
},
|
|
7608
|
+
{
|
|
7609
|
+
"flags": "--file <path>",
|
|
7610
|
+
"description": "prd.json written by the bound session"
|
|
7611
|
+
},
|
|
7612
|
+
{
|
|
7613
|
+
"flags": "--json",
|
|
7614
|
+
"description": "Print machine-readable JSON"
|
|
7615
|
+
}
|
|
7616
|
+
],
|
|
7617
|
+
"subcommands": []
|
|
7618
|
+
},
|
|
7596
7619
|
{
|
|
7597
7620
|
"name": "organize",
|
|
7598
7621
|
"description": "List or submit one-time project/task decisions (US-007B / US-005B)",
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
export declare const SECRET_NAME_PATTERN: RegExp;
|
|
2
2
|
export declare const GROUP_ID_PATTERN: RegExp;
|
|
3
3
|
export declare const EMAIL_PATTERN: RegExp;
|
|
4
|
+
/**
|
|
5
|
+
* Keep an ACL pattern exactly as the caller supplied it.
|
|
6
|
+
*
|
|
7
|
+
* `foo/` is a private folder row: it authorizes creating a direct child, but
|
|
8
|
+
* does not grant access to any child. `foo/*` is the distinct shared-folder
|
|
9
|
+
* glob. The server owns validation (including the conflict between them), so
|
|
10
|
+
* the CLI must never silently turn one into the other.
|
|
11
|
+
*/
|
|
4
12
|
export declare function normalizeFilePrefix(prefix: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Make a recursive folder glob only where the caller intentionally asks for
|
|
15
|
+
* one. This is not an ACL-input normalizer: callers must opt in because a
|
|
16
|
+
* trailing slash has private-folder semantics on ACL routes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toSharedFolderGlob(prefix: string): string;
|
|
19
|
+
/** A non-root trailing slash is the private-folder ACL pattern. */
|
|
20
|
+
export declare function isPrivateFolderPattern(prefix: string): boolean;
|
|
5
21
|
//# sourceMappingURL=_patterns.d.ts.map
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
export const SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*(?:\/[A-Z][A-Z0-9_]+)*$/;
|
|
2
2
|
export const GROUP_ID_PATTERN = /^grp_[A-Za-z0-9_-]+$/;
|
|
3
3
|
export const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
4
|
+
/**
|
|
5
|
+
* Keep an ACL pattern exactly as the caller supplied it.
|
|
6
|
+
*
|
|
7
|
+
* `foo/` is a private folder row: it authorizes creating a direct child, but
|
|
8
|
+
* does not grant access to any child. `foo/*` is the distinct shared-folder
|
|
9
|
+
* glob. The server owns validation (including the conflict between them), so
|
|
10
|
+
* the CLI must never silently turn one into the other.
|
|
11
|
+
*/
|
|
4
12
|
export function normalizeFilePrefix(prefix) {
|
|
5
|
-
if (prefix.endsWith("/")) {
|
|
6
|
-
return prefix + "*";
|
|
7
|
-
}
|
|
8
13
|
return prefix;
|
|
9
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Make a recursive folder glob only where the caller intentionally asks for
|
|
17
|
+
* one. This is not an ACL-input normalizer: callers must opt in because a
|
|
18
|
+
* trailing slash has private-folder semantics on ACL routes.
|
|
19
|
+
*/
|
|
20
|
+
export function toSharedFolderGlob(prefix) {
|
|
21
|
+
return prefix.endsWith("/") ? `${prefix}*` : prefix;
|
|
22
|
+
}
|
|
23
|
+
/** A non-root trailing slash is the private-folder ACL pattern. */
|
|
24
|
+
export function isPrivateFolderPattern(prefix) {
|
|
25
|
+
return prefix.length > 1 && prefix.endsWith("/");
|
|
26
|
+
}
|
|
10
27
|
//# sourceMappingURL=_patterns.js.map
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* `toCompanyAnchored`.
|
|
32
32
|
*
|
|
33
33
|
* Cross-package note: depends on the `VaultClient.sts.vend`/`.vendSelf`
|
|
34
|
-
* methods
|
|
34
|
+
* methods from hq-cloud.
|
|
35
35
|
*/
|
|
36
36
|
import { Command } from "commander";
|
|
37
37
|
import { ListObjectsV2Command, GetObjectCommand, type ListObjectsV2CommandOutput, type GetObjectCommandOutput } from "@aws-sdk/client-s3";
|
|
@@ -110,14 +110,18 @@ export type CompanyBrowseClientFactory = (input: {
|
|
|
110
110
|
/**
|
|
111
111
|
* ACL provenance for a single listed key.
|
|
112
112
|
* - `shared-with-you`: an explicit grant the caller holds covers the key.
|
|
113
|
+
* - `creator`: a creator-only child grant (or its folder marker) covers the
|
|
114
|
+
* key; this is explicit creator access, not an owner/admin role bypass.
|
|
113
115
|
* - `role-bypass`: the caller has no covering explicit grant, but
|
|
114
116
|
* owner/admin role widened the browse-vend policy to include it.
|
|
115
117
|
* - `personal-vault`: the key lives in the caller's own person-entity
|
|
116
118
|
* vault, where no grants graph applies — the caller is the only
|
|
117
119
|
* principal with access by construction. Emitted only when
|
|
118
120
|
* `runBrowse({ personalMode: true })`.
|
|
121
|
+
* - `private-folder`: this is the private folder row itself. Its children
|
|
122
|
+
* are creator-only, so it never covers a child key.
|
|
119
123
|
*/
|
|
120
|
-
export type AclSource = "shared-with-you" | "role-bypass" | "personal-vault";
|
|
124
|
+
export type AclSource = "shared-with-you" | "creator" | "role-bypass" | "personal-vault" | "private-folder";
|
|
121
125
|
export interface BrowseRow {
|
|
122
126
|
key: string;
|
|
123
127
|
size: number;
|
|
@@ -148,16 +152,23 @@ export declare function toBucketRelative(pathOrPrefix: string, slug: string): st
|
|
|
148
152
|
export declare function toCompanyAnchored(bucketRelKey: string, slug: string): string;
|
|
149
153
|
/**
|
|
150
154
|
* Classify a single company-relative S3 key against the caller's
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* the owner/admin role-bypass the `/sts/vend`
|
|
154
|
-
* prefix is a company-wide grant and matches
|
|
155
|
+
* raw explicit-grant patterns. `foo/*` covers all keys below `foo/`, while
|
|
156
|
+
* `foo/` is the private folder itself and never covers a child key. Otherwise
|
|
157
|
+
* the key is visible only via the owner/admin role-bypass the `/sts/vend`
|
|
158
|
+
* policy applied. An empty-string prefix is a company-wide grant and matches
|
|
159
|
+
* everything.
|
|
155
160
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
161
|
+
* The caller translates only the redundant company anchor before calling this;
|
|
162
|
+
* it must not coalesce a trailing slash into a glob, which would imply child
|
|
163
|
+
* access that the server did not grant.
|
|
159
164
|
*/
|
|
160
|
-
export declare function classifyAclSource(bucketRelKey: string,
|
|
165
|
+
export declare function classifyAclSource(bucketRelKey: string, grantPatterns: Array<string | BrowseGrantPattern>): AclSource;
|
|
166
|
+
export interface BrowseGrantPattern {
|
|
167
|
+
pattern: string;
|
|
168
|
+
source?: ExplicitGrant["source"];
|
|
169
|
+
}
|
|
170
|
+
/** Remove only the redundant company anchor; retain the ACL pattern itself. */
|
|
171
|
+
export declare function toBrowseGrantPattern(grantPath: string, slug: string): string;
|
|
161
172
|
/**
|
|
162
173
|
* Bright-line guard for `--out`: refuse to write any byte beneath
|
|
163
174
|
* `<hqRoot>/companies/`. We do NOT enumerate `companies/manifest.yaml`
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* `toCompanyAnchored`.
|
|
32
32
|
*
|
|
33
33
|
* Cross-package note: depends on the `VaultClient.sts.vend`/`.vendSelf`
|
|
34
|
-
* methods
|
|
34
|
+
* methods from hq-cloud.
|
|
35
35
|
*/
|
|
36
36
|
import { isAccessDeniedError, accessDeniedKeyOf, reportAccessDenied, accessDeniedCompanyOf, } from "../utils/access-denied-hint.js";
|
|
37
37
|
import chalk from "chalk";
|
|
@@ -40,7 +40,8 @@ import * as path from "node:path";
|
|
|
40
40
|
import { Readable } from "node:stream";
|
|
41
41
|
import { pipeline } from "node:stream/promises";
|
|
42
42
|
import { S3Client, ListObjectsV2Command, GetObjectCommand, } from "@aws-sdk/client-s3";
|
|
43
|
-
import { VaultClient,
|
|
43
|
+
import { VaultClient, } from "@indigoai-us/hq-cloud";
|
|
44
|
+
import { isPrivateFolderPattern } from "./_patterns.js";
|
|
44
45
|
import { DEFAULT_HQ_ROOT, DEFAULT_COGNITO, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
45
46
|
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
46
47
|
import { getCompanyUid, looksLikeCompanyUid, looksLikePersonVaultUid, vaultApiFetch, } from "../utils/vault-api.js";
|
|
@@ -70,9 +71,15 @@ export function parseCompanySlugFromPath(prefix) {
|
|
|
70
71
|
*/
|
|
71
72
|
export function toBucketRelative(pathOrPrefix, slug) {
|
|
72
73
|
const anchor = `companies/${slug}/`;
|
|
74
|
+
const slugAnchor = `${slug}/`;
|
|
73
75
|
const normalized = pathOrPrefix.replace(/^\/+/, "");
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
if (normalized.startsWith(anchor))
|
|
77
|
+
return normalized.slice(anchor.length);
|
|
78
|
+
// Explicit-grant responses can use either the full company anchor or the
|
|
79
|
+
// historical bare-slug anchor. Strip both before classification, while
|
|
80
|
+
// preserving the caller's trailing `/` versus `/*` semantics.
|
|
81
|
+
return normalized.startsWith(slugAnchor)
|
|
82
|
+
? normalized.slice(slugAnchor.length)
|
|
76
83
|
: normalized;
|
|
77
84
|
}
|
|
78
85
|
/**
|
|
@@ -85,22 +92,62 @@ export function toCompanyAnchored(bucketRelKey, slug) {
|
|
|
85
92
|
}
|
|
86
93
|
/**
|
|
87
94
|
* Classify a single company-relative S3 key against the caller's
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* the owner/admin role-bypass the `/sts/vend`
|
|
91
|
-
* prefix is a company-wide grant and matches
|
|
95
|
+
* raw explicit-grant patterns. `foo/*` covers all keys below `foo/`, while
|
|
96
|
+
* `foo/` is the private folder itself and never covers a child key. Otherwise
|
|
97
|
+
* the key is visible only via the owner/admin role-bypass the `/sts/vend`
|
|
98
|
+
* policy applied. An empty-string prefix is a company-wide grant and matches
|
|
99
|
+
* everything.
|
|
92
100
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
101
|
+
* The caller translates only the redundant company anchor before calling this;
|
|
102
|
+
* it must not coalesce a trailing slash into a glob, which would imply child
|
|
103
|
+
* access that the server did not grant.
|
|
96
104
|
*/
|
|
97
|
-
export function classifyAclSource(bucketRelKey,
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
export function classifyAclSource(bucketRelKey, grantPatterns) {
|
|
106
|
+
const normalized = grantPatterns.map((entry) => typeof entry === "string"
|
|
107
|
+
? { pattern: entry, source: undefined }
|
|
108
|
+
: entry);
|
|
109
|
+
for (const entry of normalized) {
|
|
110
|
+
const pattern = entry.pattern;
|
|
111
|
+
if (pattern === "" || pattern === "*")
|
|
100
112
|
return "shared-with-you";
|
|
113
|
+
if (pattern.endsWith("/*")) {
|
|
114
|
+
if (bucketRelKey.startsWith(pattern.slice(0, -1))) {
|
|
115
|
+
return entry.source === "creator" ? "creator" : "shared-with-you";
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (isPrivateFolderPattern(pattern)) {
|
|
120
|
+
if (bucketRelKey === pattern)
|
|
121
|
+
return "private-folder";
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (bucketRelKey === pattern) {
|
|
125
|
+
return entry.source === "creator" ? "creator" : "shared-with-you";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// The server can expose a creator-owned child row without returning the
|
|
129
|
+
// creator's original `foo/` row. Keep the zero-byte `foo/` marker visible
|
|
130
|
+
// and label it as creator access when a creator grant is underneath it.
|
|
131
|
+
if (bucketRelKey.endsWith("/")) {
|
|
132
|
+
const creatorOwnsMarker = normalized.some((entry) => entry.source === "creator" &&
|
|
133
|
+
entry.pattern.startsWith(bucketRelKey));
|
|
134
|
+
if (creatorOwnsMarker)
|
|
135
|
+
return "creator";
|
|
101
136
|
}
|
|
102
137
|
return "role-bypass";
|
|
103
138
|
}
|
|
139
|
+
/** Remove only the redundant company anchor; retain the ACL pattern itself. */
|
|
140
|
+
export function toBrowseGrantPattern(grantPath, slug) {
|
|
141
|
+
const companyRelative = toBucketRelative(grantPath, slug);
|
|
142
|
+
return companyRelative === "*" ? "" : companyRelative;
|
|
143
|
+
}
|
|
144
|
+
function displayAclSource(source) {
|
|
145
|
+
return source === "private-folder"
|
|
146
|
+
? "private folder — children are creator-only"
|
|
147
|
+
: source === "creator"
|
|
148
|
+
? "creator access — creator-only"
|
|
149
|
+
: source;
|
|
150
|
+
}
|
|
104
151
|
/**
|
|
105
152
|
* Bright-line guard for `--out`: refuse to write any byte beneath
|
|
106
153
|
* `<hqRoot>/companies/`. We do NOT enumerate `companies/manifest.yaml`
|
|
@@ -132,7 +179,7 @@ export function formatBrowseTable(rows) {
|
|
|
132
179
|
r.key,
|
|
133
180
|
String(r.size),
|
|
134
181
|
r.lastModified ? r.lastModified.toISOString() : "—",
|
|
135
|
-
r.aclSource,
|
|
182
|
+
displayAclSource(r.aclSource),
|
|
136
183
|
]);
|
|
137
184
|
const widths = cols.map((c, i) => Math.max(c.length, ...data.map((row) => row[i].length)));
|
|
138
185
|
const renderRow = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ");
|
|
@@ -225,14 +272,17 @@ export async function runBrowse(input) {
|
|
|
225
272
|
const listPrefix = personalMode || slug === undefined
|
|
226
273
|
? pathPrefix
|
|
227
274
|
: toBucketRelative(pathPrefix, slug);
|
|
228
|
-
// Pull the caller's explicit-grant graph once so per-key classification
|
|
229
|
-
//
|
|
230
|
-
// company
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
const
|
|
275
|
+
// Pull the caller's explicit-grant graph once so per-key classification is
|
|
276
|
+
// O(grants) without N round-trips. Retain the raw pattern after stripping
|
|
277
|
+
// only the redundant company anchor: `foo/` and `foo/*` have different
|
|
278
|
+
// server semantics. Skipped in personal mode — the grants graph is a company
|
|
279
|
+
// concept; a person bucket marks every row `"personal-vault"`.
|
|
280
|
+
const grantPatterns = personalMode || slug === undefined
|
|
234
281
|
? []
|
|
235
|
-
: (await vaultClient.listMyExplicitGrants(entityUid)).map((g) =>
|
|
282
|
+
: (await vaultClient.listMyExplicitGrants(entityUid)).map((g) => ({
|
|
283
|
+
pattern: toBrowseGrantPattern(g.path, slug),
|
|
284
|
+
source: g.source,
|
|
285
|
+
}));
|
|
236
286
|
const rows = [];
|
|
237
287
|
let continuationToken;
|
|
238
288
|
do {
|
|
@@ -244,8 +294,14 @@ export async function runBrowse(input) {
|
|
|
244
294
|
for (const obj of resp.Contents ?? []) {
|
|
245
295
|
if (!obj.Key)
|
|
246
296
|
continue;
|
|
247
|
-
|
|
248
|
-
|
|
297
|
+
const aclSource = personalMode
|
|
298
|
+
? "personal-vault"
|
|
299
|
+
: classifyAclSource(obj.Key, grantPatterns);
|
|
300
|
+
// Keep a private-folder marker visible so its ACL meaning can be shown.
|
|
301
|
+
// Other zero-byte directory markers remain list implementation details.
|
|
302
|
+
if (obj.Key.endsWith("/") &&
|
|
303
|
+
(obj.Size ?? 0) === 0 &&
|
|
304
|
+
aclSource !== "private-folder")
|
|
249
305
|
continue;
|
|
250
306
|
// `obj.Key` is company-relative. Classify in that space, then re-anchor
|
|
251
307
|
// for display so the CLI keeps speaking `companies/<slug>/...`.
|
|
@@ -255,9 +311,7 @@ export async function runBrowse(input) {
|
|
|
255
311
|
: toCompanyAnchored(obj.Key, slug),
|
|
256
312
|
size: obj.Size ?? 0,
|
|
257
313
|
lastModified: obj.LastModified,
|
|
258
|
-
aclSource
|
|
259
|
-
? "personal-vault"
|
|
260
|
-
: classifyAclSource(obj.Key, grantPrefixes),
|
|
314
|
+
aclSource,
|
|
261
315
|
});
|
|
262
316
|
}
|
|
263
317
|
continuationToken = resp.NextContinuationToken ?? undefined;
|
|
@@ -434,14 +488,23 @@ export function formatSharedWithMeTable(rows) {
|
|
|
434
488
|
return "Nothing is explicitly shared with you. (Owner/admin role-bypass access is not listed here — only explicit grants.)";
|
|
435
489
|
}
|
|
436
490
|
const cols = ["COMPANY", "PATH", "PERMISSION", "SOURCE"];
|
|
437
|
-
const data = rows.map((r) => [
|
|
491
|
+
const data = rows.map((r) => [
|
|
492
|
+
r.companySlug,
|
|
493
|
+
isPrivateFolderPattern(r.path) ? `${r.path} (private folder)` : r.path,
|
|
494
|
+
r.permission,
|
|
495
|
+
r.source,
|
|
496
|
+
]);
|
|
438
497
|
const widths = cols.map((c, i) => Math.max(c.length, ...data.map((row) => row[i].length)));
|
|
439
498
|
const renderRow = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ");
|
|
440
|
-
|
|
499
|
+
const lines = [
|
|
441
500
|
chalk.bold(renderRow(cols)),
|
|
442
501
|
chalk.dim(renderRow(widths.map((w) => "─".repeat(w)))),
|
|
443
502
|
...data.map(renderRow),
|
|
444
|
-
]
|
|
503
|
+
];
|
|
504
|
+
if (rows.some((row) => isPrivateFolderPattern(row.path))) {
|
|
505
|
+
lines.push("Private folders expose the folder only; children are creator-only.");
|
|
506
|
+
}
|
|
507
|
+
return lines.join("\n");
|
|
445
508
|
}
|
|
446
509
|
/**
|
|
447
510
|
* `hq files search <query>` orchestrator. Lists the company (or personal)
|
|
@@ -863,7 +926,7 @@ export async function resolveBrowseSession() {
|
|
|
863
926
|
export function registerFilesBrowseCommands(filesCmd) {
|
|
864
927
|
filesCmd
|
|
865
928
|
.command("browse [path]")
|
|
866
|
-
.description("List vault objects under [path] without syncing them locally. Uses the browse-vend path (role-bypass allowed). Pass --personal to browse the caller's personal vault; otherwise [path] must start with companies/<slug>/.")
|
|
929
|
+
.description("List vault objects under [path] without syncing them locally. Private-folder rows (foo/) are labelled and do not imply child access; their children are creator-only. Uses the browse-vend path (role-bypass allowed). Pass --personal to browse the caller's personal vault; otherwise [path] must start with companies/<slug>/.")
|
|
867
930
|
.option("--company <slug>", "Company slug (defaults to the slug parsed from <path>)")
|
|
868
931
|
.option("--personal", "Browse the caller's canonical personal vault. [path] is treated as " +
|
|
869
932
|
"bucket-relative (omit it to list the vault root). Mutually exclusive " +
|
|
@@ -1035,7 +1098,7 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
1035
1098
|
});
|
|
1036
1099
|
filesCmd
|
|
1037
1100
|
.command("shared-with-me")
|
|
1038
|
-
.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).")
|
|
1101
|
+
.description("List the files/prefixes explicitly shared with you. Private folders (foo/) expose only the folder; children are creator-only. 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).")
|
|
1039
1102
|
.option("--company <slug>", "Scope to a single company (defaults to a cross-company roll-up).")
|
|
1040
1103
|
.action(async (options, command) => {
|
|
1041
1104
|
try {
|