@indigoai-us/hq-cli 5.345.57 → 5.345.58
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
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.58] — 2026-10-09
|
|
6
|
+
- hq access now accepts `--as <member>` to view another member's access, read-only.
|
|
7
|
+
|
|
5
8
|
## [5.345.57] — 2026-10-09
|
|
6
9
|
- Add `hq lanes create --loop` and `hq lanes enqueue` for persistent worker lanes that process FIFO phase envelopes, return generic results or section 8 handoffs, reuse sessions per story, retry one stall, and park a second stall for the senior. Loop behavior uses the default-on `lanes.conduct-loop` hq-flags kill switch. See `docs/lanes.md` for command output and exit codes.
|
|
7
10
|
|
|
@@ -2871,6 +2871,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
2871
2871
|
readonly flags: "--company <slug>";
|
|
2872
2872
|
readonly description: "Company slug";
|
|
2873
2873
|
readonly hidden: false;
|
|
2874
|
+
}, {
|
|
2875
|
+
readonly flags: "--as <person-uid>";
|
|
2876
|
+
readonly description: "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)";
|
|
2877
|
+
readonly hidden: false;
|
|
2874
2878
|
}, {
|
|
2875
2879
|
readonly flags: "--json";
|
|
2876
2880
|
readonly description: "Print a single JSON object";
|
|
@@ -3462,6 +3462,11 @@ export const COMMAND_CATALOG = [
|
|
|
3462
3462
|
"description": "Company slug",
|
|
3463
3463
|
"hidden": false
|
|
3464
3464
|
},
|
|
3465
|
+
{
|
|
3466
|
+
"flags": "--as <person-uid>",
|
|
3467
|
+
"description": "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)",
|
|
3468
|
+
"hidden": false
|
|
3469
|
+
},
|
|
3465
3470
|
{
|
|
3466
3471
|
"flags": "--json",
|
|
3467
3472
|
"description": "Print a single JSON object",
|
|
@@ -135,6 +135,15 @@ export interface RunAccessResult {
|
|
|
135
135
|
alreadyAskedAt?: string;
|
|
136
136
|
requestNote?: string;
|
|
137
137
|
}
|
|
138
|
+
export interface AccessAsResult {
|
|
139
|
+
personUid: string;
|
|
140
|
+
company: string;
|
|
141
|
+
path: string;
|
|
142
|
+
permission: "read" | "write" | "admin" | null;
|
|
143
|
+
canRead: boolean;
|
|
144
|
+
roleBypass: boolean;
|
|
145
|
+
exitCode: 0 | 3;
|
|
146
|
+
}
|
|
138
147
|
export declare function outcomeExitCode(outcome: AccessOutcome): 0 | 2 | 3 | 4;
|
|
139
148
|
export declare function formatAccessOutcome(result: RunAccessResult): string;
|
|
140
149
|
export declare function resolveGrantor(tree: AccessAclTree | null, members: AccessMember[]): AccessGrantor | null;
|
|
@@ -149,6 +158,11 @@ export declare function resolveAccessCompany(target: string, opts?: {
|
|
|
149
158
|
company?: string;
|
|
150
159
|
hqRoot?: string;
|
|
151
160
|
}): string | undefined;
|
|
161
|
+
export declare function runAccessForPerson(rawPath: string, personUid: string, opts?: {
|
|
162
|
+
company?: string;
|
|
163
|
+
hqRoot?: string;
|
|
164
|
+
}): Promise<AccessAsResult>;
|
|
165
|
+
export declare function formatAccessForPerson(result: AccessAsResult): string;
|
|
152
166
|
export declare function runAccessForPath(path: string, opts?: {
|
|
153
167
|
company?: string;
|
|
154
168
|
hqRoot?: string;
|
package/dist/commands/access.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as readline from "node:readline";
|
|
|
20
20
|
import { ListObjectsV2Command, } from "@aws-sdk/client-s3";
|
|
21
21
|
import { DEFAULT_HQ_ROOT } from "../utils/cognito-session.js";
|
|
22
22
|
import { getCompanyUid, vaultApiFetch, resolveCallerUid } from "../utils/vault-api.js";
|
|
23
|
-
import { parseCompanySlugFromPath, resolveBrowseSession, makeCompanyPresignFactory, runGet, } from "./files-browse.js";
|
|
23
|
+
import { parseCompanySlugFromPath, classifyAclSource, resolveBrowseSession, makeCompanyPresignFactory, runGet, toBucketRelative, } from "./files-browse.js";
|
|
24
24
|
import { listJournals, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
25
25
|
import { listActiveMembers } from "./members.js";
|
|
26
26
|
import { buildDmBody } from "./dm.js";
|
|
@@ -87,6 +87,10 @@ function normalizeTarget(raw, slug) {
|
|
|
87
87
|
t = t.slice(anchor.length);
|
|
88
88
|
return t;
|
|
89
89
|
}
|
|
90
|
+
function fileGrantCoversPath(grantPath, targetPath) {
|
|
91
|
+
return classifyAclSource(targetPath, [grantPath]) !== "role-bypass";
|
|
92
|
+
}
|
|
93
|
+
const FILE_PERMISSION_RANK = { read: 1, write: 2, admin: 3 };
|
|
90
94
|
function httpStatusOf(err) {
|
|
91
95
|
if (!err || typeof err !== "object")
|
|
92
96
|
return undefined;
|
|
@@ -637,6 +641,7 @@ export function registerAccessCommand(program) {
|
|
|
637
641
|
.command("access <path-or-query>")
|
|
638
642
|
.description("Tell whether a vault file exists and whether you can read it. Never a bare not-found. Exit codes: 0 local/not-synced, 2 never-existed, 3 no-access, 4 ambiguous.")
|
|
639
643
|
.option("--company <slug>", "Company slug")
|
|
644
|
+
.option("--as <person-uid>", "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)")
|
|
640
645
|
.option("--json", "Print a single JSON object")
|
|
641
646
|
.option("--no-fix", "Diagnose only: do not fetch, pin, or repair sync")
|
|
642
647
|
.option("--yes", "Answer the one send-confirmation yes (hosts must pass this explicitly)")
|
|
@@ -650,6 +655,12 @@ Exit codes:
|
|
|
650
655
|
`)
|
|
651
656
|
.action(async (target, opts) => {
|
|
652
657
|
try {
|
|
658
|
+
if (opts.as) {
|
|
659
|
+
const result = await runAccessForPerson(target, opts.as, opts);
|
|
660
|
+
console.log(opts.json ? JSON.stringify(result) : formatAccessForPerson(result));
|
|
661
|
+
process.exitCode = result.exitCode;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
653
664
|
const result = await runAccessForPath(target, opts);
|
|
654
665
|
process.exitCode = result.exitCode;
|
|
655
666
|
}
|
|
@@ -669,6 +680,63 @@ export function resolveAccessCompany(target, opts) {
|
|
|
669
680
|
const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
|
|
670
681
|
return opts?.company ?? slugFromTarget(target) ?? readActiveCompany(hqRoot);
|
|
671
682
|
}
|
|
683
|
+
export async function runAccessForPerson(rawPath, personUid, opts) {
|
|
684
|
+
const uid = personUid.trim();
|
|
685
|
+
if (!/^(?:prs|agt)_[A-Za-z0-9_-]+$/.test(uid)) {
|
|
686
|
+
throw new Error("Pass the person's exact prs_ or agt_ UID to --as.");
|
|
687
|
+
}
|
|
688
|
+
const pathSlug = slugFromTarget(rawPath);
|
|
689
|
+
if (opts?.company && pathSlug && opts.company !== pathSlug) {
|
|
690
|
+
throw new Error("The --company value does not match the company in the path.");
|
|
691
|
+
}
|
|
692
|
+
const company = resolveAccessCompany(rawPath, opts);
|
|
693
|
+
if (!company)
|
|
694
|
+
throw new Error("Pass --company <slug> or an anchored company path.");
|
|
695
|
+
const relativePath = normalizeTarget(rawPath, company);
|
|
696
|
+
if (!relativePath || relativePath.split(/[\\/]/).includes("..")) {
|
|
697
|
+
throw new Error("Pass a company-relative path without parent-directory segments.");
|
|
698
|
+
}
|
|
699
|
+
const { token } = await resolveBrowseSession();
|
|
700
|
+
const companyUid = await getCompanyUid(token, company);
|
|
701
|
+
const response = await vaultApiFetch({
|
|
702
|
+
token,
|
|
703
|
+
path: `/files/${encodeURIComponent(companyUid)}/members/${encodeURIComponent(uid)}/access`,
|
|
704
|
+
method: "GET",
|
|
705
|
+
});
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
if (response.status === 403) {
|
|
708
|
+
throw new Error("Only the person themself or a company owner/admin can check this access.");
|
|
709
|
+
}
|
|
710
|
+
if (response.status === 404) {
|
|
711
|
+
throw new Error("That person is not an active member of this company.");
|
|
712
|
+
}
|
|
713
|
+
throw new Error(`Member access check failed (${response.status}).`);
|
|
714
|
+
}
|
|
715
|
+
const projection = (await response.json());
|
|
716
|
+
if (projection.personUid !== uid || !projection.files) {
|
|
717
|
+
throw new Error("The member access response did not match the requested person.");
|
|
718
|
+
}
|
|
719
|
+
const roleBypass = projection.files.roleBypass === true;
|
|
720
|
+
const grants = Array.isArray(projection.files.grants) ? projection.files.grants : [];
|
|
721
|
+
const matchingPermissions = grants
|
|
722
|
+
.filter((grant) => typeof grant.path === "string" && typeof grant.permission === "string" &&
|
|
723
|
+
grant.permission in FILE_PERMISSION_RANK &&
|
|
724
|
+
fileGrantCoversPath(toBucketRelative(grant.path, company), relativePath))
|
|
725
|
+
.map((grant) => grant.permission);
|
|
726
|
+
const permission = roleBypass ? "admin" : matchingPermissions.reduce((best, candidate) => best === null || FILE_PERMISSION_RANK[candidate] > FILE_PERMISSION_RANK[best] ? candidate : best, null);
|
|
727
|
+
return {
|
|
728
|
+
personUid: uid,
|
|
729
|
+
company,
|
|
730
|
+
path: `companies/${company}/${relativePath}`,
|
|
731
|
+
permission,
|
|
732
|
+
canRead: permission !== null,
|
|
733
|
+
roleBypass,
|
|
734
|
+
exitCode: permission === null ? 3 : 0,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
export function formatAccessForPerson(result) {
|
|
738
|
+
return `${result.personUid} has ${result.permission ?? "no access"} access to ${result.path}.`;
|
|
739
|
+
}
|
|
672
740
|
export async function runAccessForPath(path, opts) {
|
|
673
741
|
const target = path;
|
|
674
742
|
const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
|