@indigoai-us/hq-cli 5.108.25 → 5.109.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 +88 -0
- package/dist/commands/__fixtures__/access-vault.d.ts +93 -0
- package/dist/commands/__fixtures__/access-vault.js +166 -0
- package/dist/commands/access.d.ts +158 -0
- package/dist/commands/access.js +783 -0
- package/dist/commands/cloud.js +11 -1
- package/dist/commands/files-browse.d.ts +25 -1
- package/dist/commands/files-browse.js +81 -17
- package/dist/commands/files.js +15 -5
- package/dist/commands/integrations-api.d.ts +15 -0
- package/dist/commands/integrations-connect.js +84 -3
- package/dist/commands/integrations-oauth.js +62 -3
- package/dist/commands/mcp-registration.d.ts +17 -7
- package/dist/commands/mcp-registration.js +16 -27
- package/dist/commands/mesh.js +174 -50
- package/dist/commands/pack-install.js +5 -5
- package/dist/commands/secrets.d.ts +7 -0
- package/dist/commands/secrets.js +26 -2
- package/dist/commands/sync-mode.js +12 -1
- package/dist/commands/sync-narrow.js +12 -1
- package/dist/lib/mesh/live/backfill-held.d.ts +42 -1
- package/dist/lib/mesh/live/backfill-held.js +95 -13
- package/dist/lib/mesh/live/daemon/doctor.d.ts +15 -0
- package/dist/lib/mesh/live/daemon/doctor.js +41 -10
- package/dist/lib/mesh/live/daemon/mode.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/mode.js +88 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +8 -0
- package/dist/lib/mesh/live/daemon/run.js +39 -28
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/emit-client.d.ts +99 -0
- package/dist/lib/mesh/live/emit-client.js +193 -0
- package/dist/lib/mesh/live/emit-evidence.d.ts +49 -0
- package/dist/lib/mesh/live/emit-evidence.js +77 -0
- package/dist/lib/mesh/live/emit-replay.d.ts +26 -0
- package/dist/lib/mesh/live/emit-replay.js +157 -0
- package/dist/lib/mesh/live/emit-retry.d.ts +25 -0
- package/dist/lib/mesh/live/emit-retry.js +79 -0
- package/dist/lib/mesh/live/emit.d.ts +54 -0
- package/dist/lib/mesh/live/emit.js +153 -0
- package/dist/lib/narrow-hint-banner.d.ts +3 -7
- package/dist/lib/narrow-hint-banner.js +13 -34
- package/dist/lib/plan-limit-nag.d.ts +0 -3
- package/dist/lib/plan-limit-nag.js +10 -20
- package/dist/register-all.js +3 -0
- package/dist/utils/access-denied-hint.d.ts +32 -0
- package/dist/utils/access-denied-hint.js +139 -0
- package/dist/utils/access-requests.d.ts +28 -0
- package/dist/utils/access-requests.js +98 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.109.0] — 2026-09-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq access <path-or-query>` — one command for "I can't find or open this
|
|
10
|
+
file". Resolves an exact key or a fuzzy name (never auto-picks between
|
|
11
|
+
several hits), then reports exactly one of `never-existed`, `local`,
|
|
12
|
+
`not-synced`, or `no-access` (exit 0/2/3, 4 for ambiguous). When the file
|
|
13
|
+
exists and you can read it but it is not on disk, it fetches and pins it the
|
|
14
|
+
same way `hq files get` does, and if that fails it runs `hq sync status`,
|
|
15
|
+
`hq sync doctor`, and retries once. When you lack access it names the
|
|
16
|
+
grantor (ACL creator, then company owner/admin), shows their email, asks one
|
|
17
|
+
yes/no question, and sends them a DM whose Copy-prompt is the exact
|
|
18
|
+
`hq files share … --permission read` command. Repeat requests for the same
|
|
19
|
+
prefix are deduped for 24h via `.hq/access-requests.json`. `--json` without
|
|
20
|
+
`--yes` never sends; `--no-fix` diagnoses only.
|
|
21
|
+
- Every access-denied (403) error from `hq files …` and `hq sync …` now ends
|
|
22
|
+
with `Run: hq access <path>`; in a TTY it offers to run it for you.
|
|
23
|
+
Existing messages and exit codes are unchanged.
|
|
24
|
+
|
|
25
|
+
## [5.108.26] — 2026-09-08
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Daemon receive-only mode** (owner decision 2026-09-08; contract
|
|
30
|
+
direct-emit-v1 §6.3). `hq mesh mode <get|legacy|direct|check>` selects the
|
|
31
|
+
daemon's emit mode, resolved from `HQ_MESH_MODE` then `~/.hq/work-mesh/config.json`
|
|
32
|
+
(`emitMode`), defaulting to **legacy**. In `legacy` the daemon runs the spool
|
|
33
|
+
watch + flush + held + outbox replay + transcript-watch emit path (unchanged).
|
|
34
|
+
In `direct` it runs **receive-only** — the presence subscriber keeps the cache
|
|
35
|
+
the desktop app reads warm on `hq/{personUid}/mesh`, with no spool/held/outbox/
|
|
36
|
+
flush/transcript-watch — because sessions POST events directly. `hq mesh mode
|
|
37
|
+
check` probes whether `/v1/mesh/events` is live so the flag can flip after the
|
|
38
|
+
server route lands. `hq mesh daemon doctor` reports `mode` + `subscription`
|
|
39
|
+
state and omits the spool/held/outbox lines in direct mode.
|
|
40
|
+
- **Direct-emit path** (owner decision 2026-09-08 "retire the client daemon";
|
|
41
|
+
contract `direct-emit-v1`). `hq mesh session <kind>` now POSTs each Work Mesh
|
|
42
|
+
event straight to `POST /v1/mesh/events` with the caller's normal HQ bearer
|
|
43
|
+
token instead of appending to the local spool; the server attributes the event
|
|
44
|
+
to a company server-side and fans it out over MQTT. Evidence for attribution
|
|
45
|
+
travels in an `evidence` object gathered client-side: `companySlug` (from the
|
|
46
|
+
session meta), `cwd`, `hqRoot`, `touchedPaths` (`--touched-path`, repeatable),
|
|
47
|
+
`repoPath` (derived from the enclosing git work-tree), `project`, `task`. On a
|
|
48
|
+
network/5xx/429 failure the batch is written to a tiny local retry file that the
|
|
49
|
+
next invocation drains — no long-lived process. `--enqueue` is retained as a
|
|
50
|
+
no-op for hook back-compat (it now emits directly).
|
|
51
|
+
- `hq mesh emit` — drains the retry file; `--replay-legacy` also replays the
|
|
52
|
+
legacy `spool.jsonl` + `held.jsonl` backlog through the new endpoint, lifting
|
|
53
|
+
each event's local-only fields into `evidence` and reusing its `eventId` so the
|
|
54
|
+
server dedupes.
|
|
55
|
+
- `hq mesh daemon doctor` now reports the direct-emit path: `emit last post`
|
|
56
|
+
(lastPostAt), per-status counts from the last attempt, and `emit retry depth`.
|
|
57
|
+
- `hq integrations connect --scopes "<list>"` sends an explicit permissions list
|
|
58
|
+
on the OAuth start request, for a vendor whose requirements HQ does not yet
|
|
59
|
+
know. Space- or comma-separated; order preserved, duplicates dropped. Omitting
|
|
60
|
+
the flag is unchanged — the request carries no `scopes` field at all and
|
|
61
|
+
hq-pro keeps choosing, because an empty list would ask a vendor for no access
|
|
62
|
+
rather than for its default. Refused up front where it could never be honoured
|
|
63
|
+
(alongside `--token`, or against an app whose auth mode is known to be `key`
|
|
64
|
+
or `none`), and carried through the printed bring-your-own-client command. A
|
|
65
|
+
console hand-off cannot carry it, and now says so.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- The loopback sign-in callback page — the one HQ surface a person sees in a
|
|
70
|
+
browser during `hq integrations connect` — now carries the hq-console visual
|
|
71
|
+
language instead of an unstyled white page: near-black ground, hairline-bordered
|
|
72
|
+
card, one 13px size, grey heading over bright body, and a dim `HQ` mark.
|
|
73
|
+
Deliberately ships no webfont: the page is served by a local port and is often
|
|
74
|
+
the browser's first paint after a redirect, where a blocking font request would
|
|
75
|
+
delay it and would fail outright offline — exactly when a loopback callback
|
|
76
|
+
still works.
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- `hq secrets set` now attributes its writes to a `cli_secrets_set` telemetry
|
|
81
|
+
surface, so CLI-created secrets are no longer recorded as generic `api` traffic.
|
|
82
|
+
- `hq mesh context backfill-held` now forwards the company evidence each held
|
|
83
|
+
event recorded at enqueue time (`cwd`, `hqRoot`, the session's bound
|
|
84
|
+
`companySlug`, and any `project`/`task`) into the synthesized reconcile
|
|
85
|
+
observation, so an ended session attributes to the company it was actually
|
|
86
|
+
bound to (or its `companies/{slug}/` cwd) instead of resolving to
|
|
87
|
+
`needs_company` and staying held forever. `--dry-run` now predicts with the
|
|
88
|
+
same evidence + shared resolver the write path uses, so its `reconciled`/
|
|
89
|
+
`unresolved` counts match a real run — previously `--dry-run` reported every
|
|
90
|
+
unattributed session as would-reconcile while the write run, given no
|
|
91
|
+
evidence in its observation, resolved none.
|
|
92
|
+
|
|
5
93
|
## [5.108.25] — 2026-09-08
|
|
6
94
|
|
|
7
95
|
## [5.108.24] — 2026-09-08
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory vault / ACL / DM harness for the `hq access` command.
|
|
3
|
+
* No network I/O — listings, GetObject, STS vend, and DMs stay in process
|
|
4
|
+
* (or under a tmp hqRoot the caller supplies).
|
|
5
|
+
*/
|
|
6
|
+
import { vi } from "vitest";
|
|
7
|
+
import type { CompanyBrowseClientFactory, FilesBrowseVaultClient } from "../files-browse.js";
|
|
8
|
+
export type AccessVaultState = "missing" | "present-local" | "present-not-local" | "present-denied";
|
|
9
|
+
export interface FakeDmMessage {
|
|
10
|
+
recipient: string;
|
|
11
|
+
message: string;
|
|
12
|
+
prompt?: string;
|
|
13
|
+
details?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface FakeDmTransport {
|
|
16
|
+
sent: FakeDmMessage[];
|
|
17
|
+
send(msg: FakeDmMessage): Promise<{
|
|
18
|
+
eventId: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function makeFakeDmTransport(): FakeDmTransport;
|
|
22
|
+
export interface AclEntry {
|
|
23
|
+
granteeType: string;
|
|
24
|
+
granteeId: string;
|
|
25
|
+
permission: string;
|
|
26
|
+
grantedBy: string;
|
|
27
|
+
grantedAt: string;
|
|
28
|
+
}
|
|
29
|
+
export interface AccessAclTree {
|
|
30
|
+
prefix: string;
|
|
31
|
+
direct: AclEntry[];
|
|
32
|
+
inherited: Array<AclEntry & {
|
|
33
|
+
sourcePrefix: string;
|
|
34
|
+
}>;
|
|
35
|
+
children: Array<AclEntry & {
|
|
36
|
+
sourcePrefix: string;
|
|
37
|
+
}>;
|
|
38
|
+
directRow: {
|
|
39
|
+
creatorUid: string;
|
|
40
|
+
open: boolean;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
updatedAt: string;
|
|
43
|
+
} | null;
|
|
44
|
+
effectivePermission: string | null;
|
|
45
|
+
}
|
|
46
|
+
export interface AccessMember {
|
|
47
|
+
personUid: string;
|
|
48
|
+
personEmail: string;
|
|
49
|
+
personName?: string;
|
|
50
|
+
role: "owner" | "admin" | "member" | "guest";
|
|
51
|
+
}
|
|
52
|
+
export interface AccessVaultFixture {
|
|
53
|
+
state: AccessVaultState;
|
|
54
|
+
hqRoot: string;
|
|
55
|
+
companySlug: string;
|
|
56
|
+
companyUid: string;
|
|
57
|
+
/** Company-anchored e.g. companies/acme/knowledge/report.md */
|
|
58
|
+
key: string;
|
|
59
|
+
/** Company-relative e.g. knowledge/report.md */
|
|
60
|
+
bucketKey: string;
|
|
61
|
+
vaultClient: FilesBrowseVaultClient;
|
|
62
|
+
companyClient: CompanyBrowseClientFactory;
|
|
63
|
+
vend: ReturnType<typeof vi.fn>;
|
|
64
|
+
acl: AccessAclTree | null;
|
|
65
|
+
members: AccessMember[];
|
|
66
|
+
dm: FakeDmTransport;
|
|
67
|
+
listedKeys: string[];
|
|
68
|
+
region: string;
|
|
69
|
+
}
|
|
70
|
+
export interface MakeAccessFixtureOptions {
|
|
71
|
+
state: AccessVaultState;
|
|
72
|
+
hqRoot: string;
|
|
73
|
+
companySlug?: string;
|
|
74
|
+
key?: string;
|
|
75
|
+
extraKeys?: string[];
|
|
76
|
+
acl?: AccessAclTree | null;
|
|
77
|
+
members?: AccessMember[];
|
|
78
|
+
content?: string;
|
|
79
|
+
/**
|
|
80
|
+
* `present-denied` only: make the LIST call itself fail with 403 (the
|
|
81
|
+
* `/v1/files/list` shape) instead of returning the key. Models a caller
|
|
82
|
+
* without read on the prefix — the server refuses to enumerate.
|
|
83
|
+
*/
|
|
84
|
+
listDenied?: boolean;
|
|
85
|
+
/** Deny only the ROOT (prefix "") list — the caller can read the prefix but not enumerate the bucket. */
|
|
86
|
+
rootListDenied?: boolean;
|
|
87
|
+
}
|
|
88
|
+
export declare function vendStatusForState(state: AccessVaultState): {
|
|
89
|
+
status: number;
|
|
90
|
+
message?: string;
|
|
91
|
+
};
|
|
92
|
+
export declare function makeAccessFixture(opts: MakeAccessFixtureOptions): AccessVaultFixture;
|
|
93
|
+
//# sourceMappingURL=access-vault.d.ts.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory vault / ACL / DM harness for the `hq access` command.
|
|
3
|
+
* No network I/O — listings, GetObject, STS vend, and DMs stay in process
|
|
4
|
+
* (or under a tmp hqRoot the caller supplies).
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
9
|
+
import { vi } from "vitest";
|
|
10
|
+
import { ListObjectsV2Command, GetObjectCommand, } from "@aws-sdk/client-s3";
|
|
11
|
+
export function makeFakeDmTransport() {
|
|
12
|
+
const sent = [];
|
|
13
|
+
let n = 0;
|
|
14
|
+
return {
|
|
15
|
+
sent,
|
|
16
|
+
async send(msg) {
|
|
17
|
+
sent.push(msg);
|
|
18
|
+
n += 1;
|
|
19
|
+
return { eventId: `evt_${n}` };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const FAKE_VEND = {
|
|
24
|
+
credentials: {
|
|
25
|
+
accessKeyId: "ASIA-test",
|
|
26
|
+
secretAccessKey: "secret-test",
|
|
27
|
+
sessionToken: "session-test",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const DEFAULT_MEMBERS = [
|
|
31
|
+
{
|
|
32
|
+
personUid: "prs_owner",
|
|
33
|
+
personEmail: "owner@example.com",
|
|
34
|
+
personName: "Olive Owner",
|
|
35
|
+
role: "owner",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
personUid: "prs_admin",
|
|
39
|
+
personEmail: "admin@example.com",
|
|
40
|
+
personName: "Adam Admin",
|
|
41
|
+
role: "admin",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
personUid: "prs_me",
|
|
45
|
+
personEmail: "me@example.com",
|
|
46
|
+
personName: "Mia Member",
|
|
47
|
+
role: "member",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
export function vendStatusForState(state) {
|
|
51
|
+
if (state === "missing")
|
|
52
|
+
return { status: 404 };
|
|
53
|
+
if (state === "present-denied") {
|
|
54
|
+
return { status: 403, message: "caller lacks read on this prefix" };
|
|
55
|
+
}
|
|
56
|
+
return { status: 200 };
|
|
57
|
+
}
|
|
58
|
+
function attachStatus(err, status) {
|
|
59
|
+
const tagged = err;
|
|
60
|
+
tagged.status = status;
|
|
61
|
+
return tagged;
|
|
62
|
+
}
|
|
63
|
+
function attachHttpStatus(err, httpStatusCode) {
|
|
64
|
+
const tagged = err;
|
|
65
|
+
tagged.$metadata = { httpStatusCode };
|
|
66
|
+
return tagged;
|
|
67
|
+
}
|
|
68
|
+
function toBucketKey(key, slug) {
|
|
69
|
+
const prefix = `companies/${slug}/`;
|
|
70
|
+
const trimmed = key.replace(/^\/+/, "");
|
|
71
|
+
if (trimmed.startsWith(prefix))
|
|
72
|
+
return trimmed.slice(prefix.length);
|
|
73
|
+
return trimmed;
|
|
74
|
+
}
|
|
75
|
+
export function makeAccessFixture(opts) {
|
|
76
|
+
const companySlug = opts.companySlug ?? "acme";
|
|
77
|
+
const key = opts.key ?? `companies/${companySlug}/knowledge/report.md`;
|
|
78
|
+
const bucketKey = toBucketKey(key, companySlug);
|
|
79
|
+
const companyUid = `cmp_${companySlug}`;
|
|
80
|
+
const content = opts.content ?? "hello";
|
|
81
|
+
const extraKeys = opts.extraKeys ?? [];
|
|
82
|
+
const listedKeys = opts.state === "missing" ? [...extraKeys] : [...extraKeys, bucketKey];
|
|
83
|
+
const members = opts.members ?? DEFAULT_MEMBERS;
|
|
84
|
+
const acl = opts.acl === undefined ? null : opts.acl;
|
|
85
|
+
const region = "us-east-1";
|
|
86
|
+
const dm = makeFakeDmTransport();
|
|
87
|
+
if (opts.state === "present-local") {
|
|
88
|
+
const dest = path.join(opts.hqRoot, key);
|
|
89
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
90
|
+
fs.writeFileSync(dest, content, "utf-8");
|
|
91
|
+
}
|
|
92
|
+
const vend = vi.fn(async () => {
|
|
93
|
+
if (opts.state === "missing") {
|
|
94
|
+
throw attachStatus(new Error("not found (404)"), 404);
|
|
95
|
+
}
|
|
96
|
+
if (opts.state === "present-denied") {
|
|
97
|
+
throw attachStatus(new Error("caller lacks read on this prefix (403)"), 403);
|
|
98
|
+
}
|
|
99
|
+
return FAKE_VEND;
|
|
100
|
+
});
|
|
101
|
+
const vendSelf = vi.fn(async () => FAKE_VEND);
|
|
102
|
+
const listMyExplicitGrants = vi.fn(async () => []);
|
|
103
|
+
const entityShape = {
|
|
104
|
+
uid: companyUid,
|
|
105
|
+
slug: companySlug,
|
|
106
|
+
name: companySlug,
|
|
107
|
+
bucketName: `hq-vault-cmp-${companySlug}`,
|
|
108
|
+
};
|
|
109
|
+
const findInMyNamespace = vi.fn(async () => entityShape);
|
|
110
|
+
const get = vi.fn(async () => entityShape);
|
|
111
|
+
const vaultClient = {
|
|
112
|
+
sts: { vend, vendSelf },
|
|
113
|
+
listMyExplicitGrants,
|
|
114
|
+
entity: { get, findInMyNamespace },
|
|
115
|
+
};
|
|
116
|
+
const listDenied = opts.state === "present-denied" && opts.listDenied === true;
|
|
117
|
+
const send = vi.fn(async (cmd) => {
|
|
118
|
+
if (cmd instanceof ListObjectsV2Command) {
|
|
119
|
+
const prefix = cmd.input.Prefix ?? "";
|
|
120
|
+
if (listDenied || (opts.rootListDenied === true && prefix === "")) {
|
|
121
|
+
throw Object.assign(new Error("files list failed (403)"), {
|
|
122
|
+
status: 403,
|
|
123
|
+
key: prefix,
|
|
124
|
+
company: companySlug,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const Contents = listedKeys
|
|
128
|
+
.filter((k) => (prefix === "" ? true : k.startsWith(prefix)))
|
|
129
|
+
.map((Key) => ({
|
|
130
|
+
Key,
|
|
131
|
+
Size: content.length,
|
|
132
|
+
LastModified: new Date(),
|
|
133
|
+
}));
|
|
134
|
+
return { Contents };
|
|
135
|
+
}
|
|
136
|
+
if (cmd instanceof GetObjectCommand) {
|
|
137
|
+
const requested = cmd.input.Key ?? "";
|
|
138
|
+
if (opts.state === "missing" || requested !== bucketKey) {
|
|
139
|
+
throw attachHttpStatus(new Error("Not found (404)"), 404);
|
|
140
|
+
}
|
|
141
|
+
if (opts.state === "present-denied") {
|
|
142
|
+
throw attachHttpStatus(new Error("caller lacks read on this prefix"), 403);
|
|
143
|
+
}
|
|
144
|
+
return { Body: Readable.from([content]) };
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`unexpected command: ${cmd}`);
|
|
147
|
+
});
|
|
148
|
+
const companyClient = vi.fn(() => ({ send }));
|
|
149
|
+
return {
|
|
150
|
+
state: opts.state,
|
|
151
|
+
hqRoot: opts.hqRoot,
|
|
152
|
+
companySlug,
|
|
153
|
+
companyUid,
|
|
154
|
+
key,
|
|
155
|
+
bucketKey,
|
|
156
|
+
vaultClient,
|
|
157
|
+
companyClient,
|
|
158
|
+
vend,
|
|
159
|
+
acl,
|
|
160
|
+
members,
|
|
161
|
+
dm,
|
|
162
|
+
listedKeys,
|
|
163
|
+
region,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=access-vault.js.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq access <path-or-query>` — existence + ACL probe with a self-healing
|
|
3
|
+
* ladder: resolve, list, probe ACL, then (with --fix) download / pin / repair
|
|
4
|
+
* sync or request access from the grantor.
|
|
5
|
+
*
|
|
6
|
+
* Tells a teammate whether a vault file exists and whether they can read it,
|
|
7
|
+
* so they never see a bare not-found.
|
|
8
|
+
*
|
|
9
|
+
* Vending uses the multi-tenant STS route `vaultClient.sts.vend({ companyUid })`
|
|
10
|
+
* (`/sts/vend`). The legacy `POST /vend` is never used — it assumes a single
|
|
11
|
+
* static BUCKET_ARN that is unset in multi-tenant prod.
|
|
12
|
+
*
|
|
13
|
+
* Company vault keys are company-relative; the CLI speaks the anchored form
|
|
14
|
+
* `companies/<slug>/...` for display.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
import { type CompanyBrowseClientFactory, type FilesBrowseVaultClient, type RunGetResult } from "./files-browse.js";
|
|
18
|
+
export type AccessOutcome = "never-existed" | "local" | "not-synced" | "no-access" | "ambiguous" | "pending-confirmation";
|
|
19
|
+
export interface AccessAclTree {
|
|
20
|
+
prefix: string;
|
|
21
|
+
direct: Array<{
|
|
22
|
+
granteeType: string;
|
|
23
|
+
granteeId: string;
|
|
24
|
+
permission: string;
|
|
25
|
+
grantedBy: string;
|
|
26
|
+
grantedAt: string;
|
|
27
|
+
}>;
|
|
28
|
+
inherited: Array<{
|
|
29
|
+
granteeType: string;
|
|
30
|
+
granteeId: string;
|
|
31
|
+
permission: string;
|
|
32
|
+
grantedBy: string;
|
|
33
|
+
grantedAt: string;
|
|
34
|
+
sourcePrefix: string;
|
|
35
|
+
}>;
|
|
36
|
+
children: Array<{
|
|
37
|
+
granteeType: string;
|
|
38
|
+
granteeId: string;
|
|
39
|
+
permission: string;
|
|
40
|
+
grantedBy: string;
|
|
41
|
+
grantedAt: string;
|
|
42
|
+
sourcePrefix: string;
|
|
43
|
+
}>;
|
|
44
|
+
directRow: {
|
|
45
|
+
creatorUid: string;
|
|
46
|
+
open: boolean;
|
|
47
|
+
createdAt: string;
|
|
48
|
+
updatedAt: string;
|
|
49
|
+
} | null;
|
|
50
|
+
effectivePermission: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface AccessMember {
|
|
53
|
+
personUid: string;
|
|
54
|
+
personEmail: string;
|
|
55
|
+
personName?: string;
|
|
56
|
+
role: "owner" | "admin" | "member" | "guest";
|
|
57
|
+
}
|
|
58
|
+
export interface AccessGrantor {
|
|
59
|
+
personUid: string;
|
|
60
|
+
email: string;
|
|
61
|
+
name?: string;
|
|
62
|
+
source: "acl-creator" | "owner" | "admin";
|
|
63
|
+
}
|
|
64
|
+
export interface AccessDeps {
|
|
65
|
+
hqRoot: string;
|
|
66
|
+
companySlug: string;
|
|
67
|
+
companyUid: string;
|
|
68
|
+
vaultClient: FilesBrowseVaultClient;
|
|
69
|
+
companyClient: CompanyBrowseClientFactory;
|
|
70
|
+
region: string;
|
|
71
|
+
/** GET acl/tree for the exact company-relative prefix; null = no ACL record. */
|
|
72
|
+
fetchAclTree: (prefix: string) => Promise<AccessAclTree | null>;
|
|
73
|
+
/** Active members (owner/admin/member/guest) — used for grantor resolution. */
|
|
74
|
+
listMembers: () => Promise<AccessMember[]>;
|
|
75
|
+
requester: {
|
|
76
|
+
email: string;
|
|
77
|
+
name?: string;
|
|
78
|
+
};
|
|
79
|
+
dm?: {
|
|
80
|
+
send(msg: {
|
|
81
|
+
recipient: string;
|
|
82
|
+
message: string;
|
|
83
|
+
prompt?: string;
|
|
84
|
+
details?: string;
|
|
85
|
+
}): Promise<{
|
|
86
|
+
eventId: string;
|
|
87
|
+
}>;
|
|
88
|
+
};
|
|
89
|
+
confirm?: (question: string) => Promise<boolean>;
|
|
90
|
+
now?: () => number;
|
|
91
|
+
/** Multi-hit chooser. Absent or returning null ⇒ outcome "ambiguous". Never auto-pick. */
|
|
92
|
+
pick?: (candidates: string[]) => Promise<string | null>;
|
|
93
|
+
/** Plain-text sink (default console.log). Tests capture it. */
|
|
94
|
+
log?: (line: string) => void;
|
|
95
|
+
/**
|
|
96
|
+
* Diagnostic sink (default console.error). Carries the recipient line before
|
|
97
|
+
* a DM is sent so it is visible even under --json, whose stdout must stay
|
|
98
|
+
* a single JSON object.
|
|
99
|
+
*/
|
|
100
|
+
stderr?: (line: string) => void;
|
|
101
|
+
/** Materialize + pin one path. Default: runGet from ./files-browse.js. */
|
|
102
|
+
get?: (anchoredPath: string) => Promise<RunGetResult>;
|
|
103
|
+
/** Summarise local sync journal state for the company. */
|
|
104
|
+
syncStatus?: () => Promise<string>;
|
|
105
|
+
/** Sync doctor. Default wraps hq-cloud syncDoctor(reconcileConflicts). */
|
|
106
|
+
syncDoctor?: (apply: boolean) => Promise<SyncDoctorOutcome>;
|
|
107
|
+
}
|
|
108
|
+
export interface SyncDoctorOutcome {
|
|
109
|
+
twins: number;
|
|
110
|
+
summary: string;
|
|
111
|
+
bulkRefused?: boolean;
|
|
112
|
+
breakerMessage?: string;
|
|
113
|
+
}
|
|
114
|
+
export interface RunAccessInput {
|
|
115
|
+
target: string;
|
|
116
|
+
fix: boolean;
|
|
117
|
+
json: boolean;
|
|
118
|
+
yes?: boolean;
|
|
119
|
+
deps: AccessDeps;
|
|
120
|
+
}
|
|
121
|
+
export interface RunAccessResult {
|
|
122
|
+
outcome: AccessOutcome;
|
|
123
|
+
path: string;
|
|
124
|
+
company: string;
|
|
125
|
+
exists: boolean;
|
|
126
|
+
grantor?: AccessGrantor;
|
|
127
|
+
candidates?: string[];
|
|
128
|
+
steps: string[];
|
|
129
|
+
exitCode: 0 | 2 | 3 | 4;
|
|
130
|
+
/** True when the self-heal get rung materialized the file. Omitted when not fixed. */
|
|
131
|
+
fixed?: boolean;
|
|
132
|
+
localPath?: string;
|
|
133
|
+
requestSentAt?: string;
|
|
134
|
+
alreadyAskedAt?: string;
|
|
135
|
+
requestNote?: string;
|
|
136
|
+
}
|
|
137
|
+
export declare function outcomeExitCode(outcome: AccessOutcome): 0 | 2 | 3 | 4;
|
|
138
|
+
export declare function formatAccessOutcome(result: RunAccessResult): string;
|
|
139
|
+
export declare function resolveGrantor(tree: AccessAclTree | null, members: AccessMember[]): AccessGrantor | null;
|
|
140
|
+
export declare function runAccess(input: RunAccessInput): Promise<RunAccessResult>;
|
|
141
|
+
export declare function registerAccessCommand(program: Command): void;
|
|
142
|
+
/**
|
|
143
|
+
* Resolve the company `hq access` runs against: explicit `company` (the
|
|
144
|
+
* ladder forwards the slug stamped on a 403), else the slug anchored in the
|
|
145
|
+
* target path, else the active company in `<hqRoot>/.hq/config.json`.
|
|
146
|
+
*/
|
|
147
|
+
export declare function resolveAccessCompany(target: string, opts?: {
|
|
148
|
+
company?: string;
|
|
149
|
+
hqRoot?: string;
|
|
150
|
+
}): string | undefined;
|
|
151
|
+
export declare function runAccessForPath(path: string, opts?: {
|
|
152
|
+
company?: string;
|
|
153
|
+
hqRoot?: string;
|
|
154
|
+
json?: boolean;
|
|
155
|
+
yes?: boolean;
|
|
156
|
+
fix?: boolean;
|
|
157
|
+
}): Promise<RunAccessResult>;
|
|
158
|
+
//# sourceMappingURL=access.d.ts.map
|