@indigoai-us/hq-cli 5.55.0 → 5.57.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 +27 -0
- package/dist/commands/cloud.d.ts +9 -0
- package/dist/commands/cloud.js +47 -9
- package/dist/commands/groups.d.ts +4 -0
- package/dist/commands/groups.js +13 -8
- package/dist/commands/meetings.js +19 -8
- package/dist/commands/secrets.d.ts +2 -1
- package/dist/commands/secrets.js +98 -24
- package/dist/utils/sandbox-runner-client.d.ts +39 -0
- package/dist/utils/sandbox-runner-client.js +113 -0
- package/package.json +1 -1
- package/src/commands/cloud.push-all.test.ts +29 -0
- package/src/commands/cloud.scope-excluded-warning.test.ts +22 -0
- package/src/commands/cloud.ts +60 -11
- package/src/commands/groups.test.ts +44 -0
- package/src/commands/groups.ts +13 -6
- package/src/commands/meetings.test.ts +125 -0
- package/src/commands/meetings.ts +24 -5
- package/src/commands/secrets.test.ts +194 -1
- package/src/commands/secrets.ts +122 -24
- package/src/utils/sandbox-runner-client.test.ts +125 -0
- package/src/utils/sandbox-runner-client.ts +175 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.55.1]
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`hq meetings list` no longer crashes when a meeting has no title.** The list
|
|
10
|
+
renderer read `title.length` for column sizing and row truncation, assuming
|
|
11
|
+
every meeting has a string title. The API can return a meeting whose title is
|
|
12
|
+
null/undefined, so the command printed the `Meetings (N)` header and then died
|
|
13
|
+
with `Cannot read properties of undefined (reading 'length')`. Missing titles
|
|
14
|
+
now render as `(untitled)` and the command completes normally. (#164)
|
|
15
|
+
- **`hq meetings get <short-id>` never sends a truncated id to the by-id
|
|
16
|
+
endpoint.** A short (8-char) id prefix is now resolved to the full meeting id
|
|
17
|
+
before the lookup, so short-id `get` no longer fails or targets the wrong
|
|
18
|
+
meeting. (#163)
|
|
19
|
+
- **`hq sync push` no longer reports a false-green success while silently
|
|
20
|
+
dropping files.** When a push excluded files that fell outside the caller's
|
|
21
|
+
granted write prefixes (hit by members holding only a company-wide write grant
|
|
22
|
+
on an older client), the summary now surfaces the scope-excluded files loudly
|
|
23
|
+
instead of reporting a clean success. (#162)
|
|
24
|
+
|
|
25
|
+
## [5.55.0]
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`hq reindex --from-hook` / `--lock-timeout`** to bound the op-lock wait so a
|
|
30
|
+
hook-triggered reindex no-waits instead of blocking. (#160)
|
|
31
|
+
|
|
5
32
|
## [5.54.0]
|
|
6
33
|
|
|
7
34
|
### Added
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
import { Command } from "commander";
|
|
16
16
|
import { type ConflictStrategy, type MembershipSyncConfig, type SyncMode, type PullScope, type ExplicitGrant } from "@indigoai-us/hq-cloud";
|
|
17
17
|
import { type BannerLevel } from "../lib/narrow-hint-banner.js";
|
|
18
|
+
/**
|
|
19
|
+
* Build a loud, human-readable warning when a push dropped files because
|
|
20
|
+
* they fell outside the caller's granted write scope. Returns null when
|
|
21
|
+
* nothing was scope-excluded. Keeping this pure makes the "never silently
|
|
22
|
+
* succeed when files were dropped" guarantee unit-testable.
|
|
23
|
+
*/
|
|
24
|
+
export declare function scopeExcludedWarning(count: number): string | null;
|
|
18
25
|
export interface PullAllVaultClient {
|
|
19
26
|
listMyMemberships(): Promise<Array<{
|
|
20
27
|
companyUid: string;
|
|
@@ -140,6 +147,7 @@ export interface ShareCallResult {
|
|
|
140
147
|
bytesUploaded: number;
|
|
141
148
|
filesSkipped: number;
|
|
142
149
|
filesDeleted: number;
|
|
150
|
+
filesExcludedByScope: number;
|
|
143
151
|
conflictPaths: string[];
|
|
144
152
|
aborted: boolean;
|
|
145
153
|
}
|
|
@@ -169,6 +177,7 @@ export interface PushAllResult {
|
|
|
169
177
|
filesUploaded: number;
|
|
170
178
|
bytesUploaded: number;
|
|
171
179
|
filesDeleted: number;
|
|
180
|
+
filesExcludedByScope: number;
|
|
172
181
|
errors: Array<{
|
|
173
182
|
company: string;
|
|
174
183
|
message: string;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -13,13 +13,27 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
!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]="
|
|
16
|
+
!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]="fa72780b-ae22-5125-98b6-4e7b866b7d17")}catch(e){}}();
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
20
|
import { share, sync, getStateDir, listJournals, loadCachedTokens, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, } from "@indigoai-us/hq-cloud";
|
|
21
21
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
22
22
|
import { emitNarrowHint, isStrictRefusal, resolveBannerLevel, } from "../lib/narrow-hint-banner.js";
|
|
23
|
+
/**
|
|
24
|
+
* Build a loud, human-readable warning when a push dropped files because
|
|
25
|
+
* they fell outside the caller's granted write scope. Returns null when
|
|
26
|
+
* nothing was scope-excluded. Keeping this pure makes the "never silently
|
|
27
|
+
* succeed when files were dropped" guarantee unit-testable.
|
|
28
|
+
*/
|
|
29
|
+
export function scopeExcludedWarning(count) {
|
|
30
|
+
if (count <= 0)
|
|
31
|
+
return null;
|
|
32
|
+
return (`⚠ ${count} file(s) were NOT uploaded — they fall outside the ` +
|
|
33
|
+
`prefixes you have write access to (company-wide or direct grants). ` +
|
|
34
|
+
`They were skipped, not synced. Re-run with --json to list them, or ` +
|
|
35
|
+
`ask an admin to grant you write on those paths.`);
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* Resolve the `propagateDeletePolicy` for share() calls.
|
|
25
39
|
*
|
|
@@ -251,6 +265,7 @@ export async function pushAll(options, deps) {
|
|
|
251
265
|
filesUploaded: 0,
|
|
252
266
|
bytesUploaded: 0,
|
|
253
267
|
filesDeleted: 0,
|
|
268
|
+
filesExcludedByScope: 0,
|
|
254
269
|
errors: [],
|
|
255
270
|
perCompany: [],
|
|
256
271
|
};
|
|
@@ -261,6 +276,7 @@ export async function pushAll(options, deps) {
|
|
|
261
276
|
result.filesUploaded += r.filesUploaded;
|
|
262
277
|
result.bytesUploaded += r.bytesUploaded;
|
|
263
278
|
result.filesDeleted += r.filesDeleted;
|
|
279
|
+
result.filesExcludedByScope += r.filesExcludedByScope;
|
|
264
280
|
result.perCompany.push({ slug: entry.slug, result: r });
|
|
265
281
|
}
|
|
266
282
|
catch (err) {
|
|
@@ -621,7 +637,14 @@ export function registerCloudCommands(program) {
|
|
|
621
637
|
log(chalk.yellow(`\n⚠ Push aborted (${result.filesUploaded} uploaded, ${result.filesSkipped} skipped)`));
|
|
622
638
|
process.exit(1);
|
|
623
639
|
}
|
|
624
|
-
|
|
640
|
+
if (result.filesExcludedByScope > 0) {
|
|
641
|
+
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
642
|
+
`${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`));
|
|
643
|
+
log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
|
|
647
|
+
}
|
|
625
648
|
}
|
|
626
649
|
catch (err) {
|
|
627
650
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1051,18 +1074,27 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1051
1074
|
}
|
|
1052
1075
|
else if (row.result) {
|
|
1053
1076
|
const r = row.result;
|
|
1054
|
-
const status = r.aborted
|
|
1077
|
+
const status = r.aborted || r.filesExcludedByScope > 0
|
|
1078
|
+
? chalk.yellow("⚠")
|
|
1079
|
+
: chalk.green("✓");
|
|
1055
1080
|
console.log(` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
|
|
1056
1081
|
`${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
|
|
1057
|
-
`${r.filesDeleted} deleted, ${r.
|
|
1082
|
+
`${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
|
|
1083
|
+
`${r.conflictPaths.length} conflict(s)` +
|
|
1058
1084
|
(r.aborted ? " — aborted" : ""));
|
|
1059
1085
|
}
|
|
1060
1086
|
}
|
|
1061
1087
|
const errored = result.errors.length;
|
|
1062
1088
|
const summary = `\nPushed ${result.filesUploaded} file(s) ` +
|
|
1063
1089
|
`(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
|
|
1064
|
-
`target(s); ${result.filesDeleted} deleted; ${errored} error(s)
|
|
1065
|
-
|
|
1090
|
+
`target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
|
|
1091
|
+
`${result.filesExcludedByScope} scope-excluded`;
|
|
1092
|
+
console.log(errored > 0 || result.filesExcludedByScope > 0
|
|
1093
|
+
? chalk.yellow(summary)
|
|
1094
|
+
: chalk.green(summary));
|
|
1095
|
+
if (result.filesExcludedByScope > 0) {
|
|
1096
|
+
console.log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
|
|
1097
|
+
}
|
|
1066
1098
|
if (errored > 0)
|
|
1067
1099
|
process.exit(1);
|
|
1068
1100
|
}
|
|
@@ -1124,10 +1156,16 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1124
1156
|
...(journalSlug !== undefined ? { journalSlug } : {}),
|
|
1125
1157
|
...(author ? { author } : {}),
|
|
1126
1158
|
});
|
|
1127
|
-
|
|
1159
|
+
const pushStatus = pushResult.aborted || pushResult.filesExcludedByScope > 0
|
|
1160
|
+
? chalk.yellow("⚠")
|
|
1161
|
+
: chalk.green("✓");
|
|
1162
|
+
console.log(` ${pushStatus} ` +
|
|
1128
1163
|
`${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
|
|
1129
|
-
`${pushResult.filesDeleted} deleted` +
|
|
1164
|
+
`${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
|
|
1130
1165
|
(pushResult.aborted ? " — aborted" : ""));
|
|
1166
|
+
if (pushResult.filesExcludedByScope > 0) {
|
|
1167
|
+
console.log(chalk.yellow(scopeExcludedWarning(pushResult.filesExcludedByScope)));
|
|
1168
|
+
}
|
|
1131
1169
|
if (pushResult.aborted) {
|
|
1132
1170
|
console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
|
|
1133
1171
|
process.exit(1);
|
|
@@ -1330,4 +1368,4 @@ function resolveUploadAuthorFromCache() {
|
|
|
1330
1368
|
}
|
|
1331
1369
|
}
|
|
1332
1370
|
//# sourceMappingURL=cloud.js.map
|
|
1333
|
-
//# debugId=
|
|
1371
|
+
//# debugId=fa72780b-ae22-5125-98b6-4e7b866b7d17
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
export declare function detectPrincipalType(principal: string): {
|
|
3
|
+
granteeType: "email" | "person";
|
|
4
|
+
granteeId: string;
|
|
5
|
+
} | null;
|
|
2
6
|
export declare function registerGroupsCommand(program: Command): void;
|
|
3
7
|
//# sourceMappingURL=groups.d.ts.map
|
package/dist/commands/groups.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
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]="
|
|
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]="caec6a8e-2b28-583a-acb3-1e70ab6c390c")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
5
|
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
6
6
|
import { GROUP_ID_PATTERN } from "./_patterns.js";
|
|
7
7
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
8
8
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
9
|
-
|
|
9
|
+
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/;
|
|
10
|
+
const INVALID_PRINCIPAL_HINT = "must be an email address, a personUid (prs_…), or an agentUid (agt_…)";
|
|
11
|
+
export function detectPrincipalType(principal) {
|
|
10
12
|
if (EMAIL_PATTERN.test(principal)) {
|
|
11
13
|
// Server normalizes email again; we normalize here so local validation /
|
|
12
14
|
// cache keys agree with the server-side canonicalization.
|
|
13
15
|
return { granteeType: "email", granteeId: principal.trim().toLowerCase() };
|
|
14
16
|
}
|
|
15
|
-
|
|
17
|
+
// Agent uids ride the same personUid wire slot as people — that is the
|
|
18
|
+
// server contract (group members, DM recipients, and memberships all carry
|
|
19
|
+
// agt_* in the personUid field; see hq-pro handlers).
|
|
20
|
+
if (PERSON_UID_PATTERN.test(principal) || AGENT_UID_PATTERN.test(principal)) {
|
|
16
21
|
return { granteeType: "person", granteeId: principal };
|
|
17
22
|
}
|
|
18
23
|
return null;
|
|
@@ -124,7 +129,7 @@ export function registerGroupsCommand(program) {
|
|
|
124
129
|
});
|
|
125
130
|
groups
|
|
126
131
|
.command("add <groupId> <principal>")
|
|
127
|
-
.description("Add a person to a group (principal: email or
|
|
132
|
+
.description("Add a person or agent to a group (principal: email, personUid, or agentUid)")
|
|
128
133
|
.action(async (groupId, principal) => {
|
|
129
134
|
try {
|
|
130
135
|
if (!GROUP_ID_PATTERN.test(groupId)) {
|
|
@@ -133,7 +138,7 @@ export function registerGroupsCommand(program) {
|
|
|
133
138
|
}
|
|
134
139
|
const detected = detectPrincipalType(principal);
|
|
135
140
|
if (!detected) {
|
|
136
|
-
console.error(chalk.red(`Invalid principal '${principal}':
|
|
141
|
+
console.error(chalk.red(`Invalid principal '${principal}': ${INVALID_PRINCIPAL_HINT}`));
|
|
137
142
|
process.exit(1);
|
|
138
143
|
}
|
|
139
144
|
const token = await ensureCognitoToken();
|
|
@@ -174,7 +179,7 @@ export function registerGroupsCommand(program) {
|
|
|
174
179
|
});
|
|
175
180
|
groups
|
|
176
181
|
.command("remove <groupId> <principal>")
|
|
177
|
-
.description("Remove a person from a group (principal: email or
|
|
182
|
+
.description("Remove a person or agent from a group (principal: email, personUid, or agentUid)")
|
|
178
183
|
.action(async (groupId, principal) => {
|
|
179
184
|
try {
|
|
180
185
|
if (!GROUP_ID_PATTERN.test(groupId)) {
|
|
@@ -183,7 +188,7 @@ export function registerGroupsCommand(program) {
|
|
|
183
188
|
}
|
|
184
189
|
const detected = detectPrincipalType(principal);
|
|
185
190
|
if (!detected) {
|
|
186
|
-
console.error(chalk.red(`Invalid principal '${principal}':
|
|
191
|
+
console.error(chalk.red(`Invalid principal '${principal}': ${INVALID_PRINCIPAL_HINT}`));
|
|
187
192
|
process.exit(1);
|
|
188
193
|
}
|
|
189
194
|
const token = await ensureCognitoToken();
|
|
@@ -345,4 +350,4 @@ export function registerGroupsCommand(program) {
|
|
|
345
350
|
});
|
|
346
351
|
}
|
|
347
352
|
//# sourceMappingURL=groups.js.map
|
|
348
|
-
//# debugId=
|
|
353
|
+
//# debugId=caec6a8e-2b28-583a-acb3-1e70ab6c390c
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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]="
|
|
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]="f2fc6ee0-c7c4-5012-85ce-aea36030aa92")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
5
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
@@ -35,9 +35,14 @@ function statusBadge(status) {
|
|
|
35
35
|
async function resolveShortId(token, prefix, query) {
|
|
36
36
|
if (prefix.includes("-") && prefix.length > 8)
|
|
37
37
|
return prefix;
|
|
38
|
-
const res = await vaultApiFetch({
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const res = await vaultApiFetch({
|
|
39
|
+
token,
|
|
40
|
+
path: "/v1/meetings",
|
|
41
|
+
query: { ...query, limit: "500" },
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(`Could not resolve meeting ID "${prefix}" — failed to list meetings (${res.status}). Pass the full meeting id.`);
|
|
45
|
+
}
|
|
41
46
|
const data = (await res.json());
|
|
42
47
|
const matches = data.meetings.filter((m) => m.meetingId.startsWith(prefix));
|
|
43
48
|
if (matches.length === 1)
|
|
@@ -46,7 +51,8 @@ async function resolveShortId(token, prefix, query) {
|
|
|
46
51
|
console.error(chalk.red(`Ambiguous ID prefix "${prefix}" — matches ${matches.length} meetings. Use a longer prefix.`));
|
|
47
52
|
process.exit(1);
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
console.error(chalk.red(`No meeting matches ID "${prefix}". It may be older than the meetings shown by \`hq meetings list\`, still processing, or attributed to a different company. Pass the full meeting id, add --company <slug>, or widen the list with \`hq meetings list --limit <n>\`.`));
|
|
55
|
+
process.exit(1);
|
|
50
56
|
}
|
|
51
57
|
async function handleApiError(res) {
|
|
52
58
|
const body = (await res.json().catch(() => ({})));
|
|
@@ -69,8 +75,12 @@ function printMeetingTable(meetings) {
|
|
|
69
75
|
console.log(chalk.dim(" No meetings found."));
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
78
|
+
// Titles can come back null/undefined from the API even though the type says
|
|
79
|
+
// string; fall back to a placeholder so width calc + rendering never crash on
|
|
80
|
+
// `undefined.length`.
|
|
81
|
+
const displayTitle = (m) => m.title ?? "(untitled)";
|
|
72
82
|
const ID_W = 8;
|
|
73
|
-
const TITLE_W = Math.min(40, Math.max(10, ...meetings.map((m) => m.
|
|
83
|
+
const TITLE_W = Math.min(40, Math.max(10, ...meetings.map((m) => displayTitle(m).length)));
|
|
74
84
|
const DATE_W = 16;
|
|
75
85
|
const DUR_W = 8;
|
|
76
86
|
const STATUS_W = 12;
|
|
@@ -87,7 +97,8 @@ function printMeetingTable(meetings) {
|
|
|
87
97
|
].join(" ")));
|
|
88
98
|
for (const m of meetings) {
|
|
89
99
|
const id = m.meetingId.slice(0, 8);
|
|
90
|
-
const
|
|
100
|
+
const fullTitle = displayTitle(m);
|
|
101
|
+
const title = fullTitle.length > TITLE_W ? fullTitle.slice(0, TITLE_W - 1) + "…" : fullTitle;
|
|
91
102
|
const date = new Date(m.startTime).toLocaleDateString("en-US", {
|
|
92
103
|
month: "short",
|
|
93
104
|
day: "numeric",
|
|
@@ -426,4 +437,4 @@ export function registerMeetingsCommand(program) {
|
|
|
426
437
|
});
|
|
427
438
|
}
|
|
428
439
|
//# sourceMappingURL=meetings.js.map
|
|
429
|
-
//# debugId=
|
|
440
|
+
//# debugId=f2fc6ee0-c7c4-5012-85ce-aea36030aa92
|
|
@@ -4,7 +4,7 @@ export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
|
4
4
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
5
5
|
export type SecretTier = "standard" | "sensitive" | "nuclear";
|
|
6
6
|
export type SecretScriptLockMode = "off" | "enforced";
|
|
7
|
-
export type SecretUsageChannel = "run" | "exec" | "env" | "reveal" | "submit-link";
|
|
7
|
+
export type SecretUsageChannel = "run" | "exec" | "env" | "sandbox" | "reveal" | "submit-link";
|
|
8
8
|
export interface SecretScriptUsage {
|
|
9
9
|
scriptId: string;
|
|
10
10
|
path: string;
|
|
@@ -36,6 +36,7 @@ export interface SecretLoadResponse {
|
|
|
36
36
|
message?: string;
|
|
37
37
|
}>;
|
|
38
38
|
}
|
|
39
|
+
export declare function scrubSandboxOutput(text: string, secretNames?: string[]): string;
|
|
39
40
|
export declare function loadRevealedSecrets(token: string, companyUid: string, keys: string[], usage?: SecretUsage): Promise<Map<string, string>>;
|
|
40
41
|
export declare function registerSecretsCommand(program: Command): void;
|
|
41
42
|
//# sourceMappingURL=secrets.d.ts.map
|
package/dist/commands/secrets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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]="
|
|
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]="18312d3d-7daf-5faf-ac29-c82ce7bd0b29")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -10,6 +10,7 @@ import { computeSha256 } from "../utils/integrity.js";
|
|
|
10
10
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
|
|
11
11
|
import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
|
|
12
12
|
import { vaultApiFetch, getCompanyUid, getEntityUid, } from "../utils/vault-api.js";
|
|
13
|
+
import { SandboxRunnerClient, } from "../utils/sandbox-runner-client.js";
|
|
13
14
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
14
15
|
function scopeOpts(opts) {
|
|
15
16
|
if (opts.personal && opts.company) {
|
|
@@ -183,6 +184,49 @@ async function buildSecretUsage(channel, scriptPath, scriptId, attestationLevel
|
|
|
183
184
|
},
|
|
184
185
|
};
|
|
185
186
|
}
|
|
187
|
+
function parseSecretNameList(input) {
|
|
188
|
+
const keys = input.split(",").map((k) => k.trim()).filter(Boolean);
|
|
189
|
+
if (keys.length === 0) {
|
|
190
|
+
console.error(chalk.red("Error: --only requires at least one secret name."));
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
for (const key of keys) {
|
|
194
|
+
if (!SECRET_NAME_PATTERN.test(key)) {
|
|
195
|
+
console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$`));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return keys;
|
|
200
|
+
}
|
|
201
|
+
function mergeScopeOpts(parent, child) {
|
|
202
|
+
return {
|
|
203
|
+
company: child.company ?? parent.company,
|
|
204
|
+
personal: child.personal ?? parent.personal,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export function scrubSandboxOutput(text, secretNames = []) {
|
|
208
|
+
let scrubbed = text;
|
|
209
|
+
for (const name of secretNames) {
|
|
210
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
211
|
+
scrubbed = scrubbed.replace(new RegExp(`\\b(${escaped})\\s*=\\s*([^\\s'"\\n]+|'[^'\\n]*'|"[^"\\n]*")`, "g"), "$1=[REDACTED]");
|
|
212
|
+
scrubbed = scrubbed.replace(new RegExp(`\\b(${escaped})\\s*:\\s*([^\\s'"\\n]+|'[^'\\n]*'|"[^"\\n]*")`, "g"), "$1: [REDACTED]");
|
|
213
|
+
}
|
|
214
|
+
return scrubbed
|
|
215
|
+
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]")
|
|
216
|
+
.replace(/\bgh[pousr]_[A-Za-z0-9_]{16,}\b/g, "[REDACTED]")
|
|
217
|
+
.replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[REDACTED]");
|
|
218
|
+
}
|
|
219
|
+
function renderSandboxJobResult(job, secretNames) {
|
|
220
|
+
if (job.stdout) {
|
|
221
|
+
process.stdout.write(scrubSandboxOutput(job.stdout, secretNames));
|
|
222
|
+
}
|
|
223
|
+
if (job.stderr) {
|
|
224
|
+
process.stderr.write(scrubSandboxOutput(job.stderr, secretNames));
|
|
225
|
+
}
|
|
226
|
+
if (job.logsTail) {
|
|
227
|
+
process.stderr.write(scrubSandboxOutput(job.logsTail, secretNames));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
186
230
|
function normalizePolicyRecord(secretPath, data) {
|
|
187
231
|
const policy = data.policy ?? { path: secretPath };
|
|
188
232
|
const scripts = Array.isArray(policy.scripts)
|
|
@@ -826,6 +870,56 @@ export function registerSecretsCommand(program) {
|
|
|
826
870
|
process.exit(1);
|
|
827
871
|
}
|
|
828
872
|
});
|
|
873
|
+
secrets
|
|
874
|
+
.command("sandbox")
|
|
875
|
+
.description("Run a skill in the hosted sandbox with secrets injected server-side")
|
|
876
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
877
|
+
.option("--personal", "Operate on the caller's personal vault (no sharing)")
|
|
878
|
+
.option("--only <keys>", "Comma-separated list of secret names the skill may use")
|
|
879
|
+
.option("--script <path>", "Attach local script identity for script-locked secrets")
|
|
880
|
+
.allowUnknownOption(true)
|
|
881
|
+
.action(async (opts, cmd) => {
|
|
882
|
+
try {
|
|
883
|
+
const dashIndex = process.argv.indexOf("--");
|
|
884
|
+
const rawArgs = dashIndex !== -1 ? process.argv.slice(dashIndex + 1) : cmd.args;
|
|
885
|
+
if (rawArgs.length === 0) {
|
|
886
|
+
console.error(chalk.red("Error: no skill specified. Usage: hq secrets sandbox [--company X] [--only KEY1,KEY2] -- <skill> [args...]"));
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
const [skillId, ...skillArgs] = rawArgs;
|
|
890
|
+
const keys = opts.only ? parseSecretNameList(opts.only) : [];
|
|
891
|
+
const token = await ensureCognitoToken();
|
|
892
|
+
const scope = scopeOpts(mergeScopeOpts(secrets.opts(), opts));
|
|
893
|
+
const companyUid = await getEntityUid(token, scope);
|
|
894
|
+
const usage = await buildSecretUsage("sandbox", opts.script);
|
|
895
|
+
const client = new SandboxRunnerClient();
|
|
896
|
+
const started = await client.startJob(token, {
|
|
897
|
+
skillId,
|
|
898
|
+
companyUid,
|
|
899
|
+
args: skillArgs.length > 0 ? { argv: skillArgs } : undefined,
|
|
900
|
+
companySlug: scope.companySlug,
|
|
901
|
+
only: keys.length > 0 ? keys : undefined,
|
|
902
|
+
usage,
|
|
903
|
+
});
|
|
904
|
+
const job = started.status === "succeeded" || started.status === "failed"
|
|
905
|
+
? await client.getJob(token, started.jobId)
|
|
906
|
+
: await client.pollJob(token, started.jobId);
|
|
907
|
+
renderSandboxJobResult(job, keys);
|
|
908
|
+
if (job.status === "failed") {
|
|
909
|
+
if (job.error) {
|
|
910
|
+
console.error(chalk.red("Sandbox job failed:"), scrubSandboxOutput(job.error, keys));
|
|
911
|
+
}
|
|
912
|
+
process.exit(job.exitCode && job.exitCode > 0 ? job.exitCode : 1);
|
|
913
|
+
}
|
|
914
|
+
if (job.exitCode && job.exitCode !== 0) {
|
|
915
|
+
process.exit(job.exitCode);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
catch (err) {
|
|
919
|
+
console.error(chalk.red("Error:"), err instanceof Error ? scrubSandboxOutput(err.message) : scrubSandboxOutput(String(err)));
|
|
920
|
+
process.exit(1);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
829
923
|
secrets
|
|
830
924
|
.command("exec")
|
|
831
925
|
.description("Run a command with secrets injected as env vars")
|
|
@@ -847,17 +941,7 @@ export function registerSecretsCommand(program) {
|
|
|
847
941
|
console.error(chalk.red("Error: no command specified. Usage: hq secrets exec --only KEY1,KEY2 -- <command>"));
|
|
848
942
|
process.exit(1);
|
|
849
943
|
}
|
|
850
|
-
const keys = _opts.only
|
|
851
|
-
if (keys.length === 0) {
|
|
852
|
-
console.error(chalk.red("Error: --only requires at least one secret name."));
|
|
853
|
-
process.exit(1);
|
|
854
|
-
}
|
|
855
|
-
for (const key of keys) {
|
|
856
|
-
if (!SECRET_NAME_PATTERN.test(key)) {
|
|
857
|
-
console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$`));
|
|
858
|
-
process.exit(1);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
944
|
+
const keys = parseSecretNameList(_opts.only);
|
|
861
945
|
const token = await ensureCognitoToken();
|
|
862
946
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
863
947
|
const revealed = await loadRevealedSecrets(token, companyUid, keys, await buildSecretUsage("exec", _opts.script));
|
|
@@ -903,17 +987,7 @@ export function registerSecretsCommand(program) {
|
|
|
903
987
|
if (redact) {
|
|
904
988
|
console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
|
|
905
989
|
}
|
|
906
|
-
const keys = opts.only
|
|
907
|
-
if (keys.length === 0) {
|
|
908
|
-
console.error(chalk.red("Error: --only requires at least one secret name."));
|
|
909
|
-
process.exit(1);
|
|
910
|
-
}
|
|
911
|
-
for (const key of keys) {
|
|
912
|
-
if (!SECRET_NAME_PATTERN.test(key)) {
|
|
913
|
-
console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$`));
|
|
914
|
-
process.exit(1);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
990
|
+
const keys = parseSecretNameList(opts.only);
|
|
917
991
|
const token = await ensureCognitoToken();
|
|
918
992
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
919
993
|
const revealed = await loadRevealedSecrets(token, companyUid, keys, await buildSecretUsage("env", opts.script));
|
|
@@ -1187,4 +1261,4 @@ export function registerSecretsCommand(program) {
|
|
|
1187
1261
|
});
|
|
1188
1262
|
}
|
|
1189
1263
|
//# sourceMappingURL=secrets.js.map
|
|
1190
|
-
//# debugId=
|
|
1264
|
+
//# debugId=18312d3d-7daf-5faf-ac29-c82ce7bd0b29
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type SandboxRunnerState = "queued" | "running" | "succeeded" | "failed";
|
|
2
|
+
export interface SandboxRunnerStartRequest {
|
|
3
|
+
skillId: string;
|
|
4
|
+
companyUid: string;
|
|
5
|
+
args?: Record<string, unknown>;
|
|
6
|
+
companySlug?: string;
|
|
7
|
+
only?: string[];
|
|
8
|
+
usage?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface SandboxRunnerStartResponse {
|
|
11
|
+
jobId: string;
|
|
12
|
+
status: SandboxRunnerState;
|
|
13
|
+
}
|
|
14
|
+
export interface SandboxRunnerJob {
|
|
15
|
+
jobId: string;
|
|
16
|
+
status: SandboxRunnerState;
|
|
17
|
+
stdout?: string;
|
|
18
|
+
stderr?: string;
|
|
19
|
+
logsTail?: string;
|
|
20
|
+
exitCode?: number;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SandboxRunnerClientOptions {
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
fetchImpl?: typeof fetch;
|
|
26
|
+
}
|
|
27
|
+
export interface SandboxRunnerPollOptions {
|
|
28
|
+
intervalMs?: number;
|
|
29
|
+
maxPolls?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class SandboxRunnerClient {
|
|
32
|
+
private readonly baseUrl;
|
|
33
|
+
private readonly fetchImpl;
|
|
34
|
+
constructor(options?: SandboxRunnerClientOptions);
|
|
35
|
+
startJob(token: string, request: SandboxRunnerStartRequest): Promise<SandboxRunnerStartResponse>;
|
|
36
|
+
getJob(token: string, jobId: string): Promise<SandboxRunnerJob>;
|
|
37
|
+
pollJob(token: string, jobId: string, options?: SandboxRunnerPollOptions): Promise<SandboxRunnerJob>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=sandbox-runner-client.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
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]="8203259b-d3ae-5c9b-845e-f4c1470c30f3")}catch(e){}}();
|
|
3
|
+
const DEFAULT_SANDBOX_RUNNER_URL = "https://hqapi.getindigo.ai/sandbox";
|
|
4
|
+
function normalizeBaseUrl(baseUrl) {
|
|
5
|
+
return baseUrl.replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
function getSandboxRunnerBaseUrl() {
|
|
8
|
+
return normalizeBaseUrl(process.env.HQ_SANDBOX_RUNNER_URL ?? DEFAULT_SANDBOX_RUNNER_URL);
|
|
9
|
+
}
|
|
10
|
+
function isSandboxRunnerState(value) {
|
|
11
|
+
return value === "queued" || value === "running" || value === "succeeded" || value === "failed";
|
|
12
|
+
}
|
|
13
|
+
async function parseJsonResponse(res) {
|
|
14
|
+
return (await res.json().catch(() => ({})));
|
|
15
|
+
}
|
|
16
|
+
function requireString(body, key) {
|
|
17
|
+
const value = body[key];
|
|
18
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
19
|
+
throw new Error(`Sandbox Runner returned an invalid '${key}'.`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function normalizeJob(body, jobIdFallback) {
|
|
24
|
+
const status = body.status;
|
|
25
|
+
if (!isSandboxRunnerState(status)) {
|
|
26
|
+
throw new Error("Sandbox Runner returned an invalid job status.");
|
|
27
|
+
}
|
|
28
|
+
const output = typeof body.stdout === "string"
|
|
29
|
+
? body.stdout
|
|
30
|
+
: typeof body.output === "string"
|
|
31
|
+
? body.output
|
|
32
|
+
: undefined;
|
|
33
|
+
return {
|
|
34
|
+
jobId: typeof body.jobId === "string" && body.jobId.length > 0
|
|
35
|
+
? body.jobId
|
|
36
|
+
: jobIdFallback ?? requireString(body, "jobId"),
|
|
37
|
+
status,
|
|
38
|
+
stdout: output,
|
|
39
|
+
stderr: typeof body.stderr === "string" ? body.stderr : undefined,
|
|
40
|
+
logsTail: typeof body.logsTail === "string" ? body.logsTail : undefined,
|
|
41
|
+
exitCode: typeof body.exitCode === "number" ? body.exitCode : undefined,
|
|
42
|
+
error: typeof body.error === "string" ? body.error : undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function delay(ms) {
|
|
46
|
+
if (ms <= 0)
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
+
}
|
|
50
|
+
export class SandboxRunnerClient {
|
|
51
|
+
baseUrl;
|
|
52
|
+
fetchImpl;
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl ?? getSandboxRunnerBaseUrl());
|
|
55
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
56
|
+
}
|
|
57
|
+
async startJob(token, request) {
|
|
58
|
+
const res = await this.fetchImpl(`${this.baseUrl}/jobs`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${token}`,
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(request),
|
|
65
|
+
});
|
|
66
|
+
const body = await parseJsonResponse(res);
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const message = typeof body.message === "string"
|
|
69
|
+
? body.message
|
|
70
|
+
: typeof body.error === "string"
|
|
71
|
+
? body.error
|
|
72
|
+
: res.statusText;
|
|
73
|
+
throw new Error(`Sandbox Runner rejected job: ${message}`);
|
|
74
|
+
}
|
|
75
|
+
const status = body.status;
|
|
76
|
+
if (!isSandboxRunnerState(status)) {
|
|
77
|
+
throw new Error("Sandbox Runner returned an invalid start status.");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
jobId: requireString(body, "jobId"),
|
|
81
|
+
status,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async getJob(token, jobId) {
|
|
85
|
+
const res = await this.fetchImpl(`${this.baseUrl}/jobs/${encodeURIComponent(jobId)}`, {
|
|
86
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
87
|
+
});
|
|
88
|
+
const body = await parseJsonResponse(res);
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
const message = typeof body.message === "string"
|
|
91
|
+
? body.message
|
|
92
|
+
: typeof body.error === "string"
|
|
93
|
+
? body.error
|
|
94
|
+
: res.statusText;
|
|
95
|
+
throw new Error(`Sandbox Runner job lookup failed: ${message}`);
|
|
96
|
+
}
|
|
97
|
+
return normalizeJob(body, jobId);
|
|
98
|
+
}
|
|
99
|
+
async pollJob(token, jobId, options = {}) {
|
|
100
|
+
const intervalMs = options.intervalMs ?? 1000;
|
|
101
|
+
const maxPolls = options.maxPolls ?? 300;
|
|
102
|
+
for (let attempt = 0; attempt < maxPolls; attempt += 1) {
|
|
103
|
+
const job = await this.getJob(token, jobId);
|
|
104
|
+
if (job.status === "succeeded" || job.status === "failed") {
|
|
105
|
+
return job;
|
|
106
|
+
}
|
|
107
|
+
await delay(intervalMs);
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`Sandbox Runner job '${jobId}' did not finish before the poll limit.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=sandbox-runner-client.js.map
|
|
113
|
+
//# debugId=8203259b-d3ae-5c9b-845e-f4c1470c30f3
|