@indigoai-us/hq-cli 5.345.155 → 5.345.159
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 +289 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +536 -7
- package/dist/command-catalog.generated.js +676 -24
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-inbox.js +2 -2
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +13 -1
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files-browse.d.ts +16 -0
- package/dist/commands/files-browse.js +52 -1
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/integrations-core.d.ts +13 -1
- package/dist/commands/integrations-core.js +49 -4
- package/dist/commands/integrations.js +4 -2
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +185 -16
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
- package/dist/lib/agent-kit/inbox-state.js +5 -1
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
- package/dist/lib/doctor/checks/sync-health.js +66 -1
- package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
- package/dist/lib/doctor/company-resolution-state.js +89 -0
- package/dist/lib/doctor/fix/apply.d.ts +36 -6
- package/dist/lib/doctor/fix/apply.js +128 -12
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.js +6 -2
- package/dist/lib/index-render/company-knowledge.d.ts +1 -1
- package/dist/lib/index-render/company-knowledge.js +4 -5
- package/dist/lib/index-render/index.js +44 -3
- package/dist/lib/index-render/projects.d.ts +1 -1
- package/dist/lib/index-render/projects.js +5 -5
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/access-denied-hint.js +6 -0
- package/dist/utils/client-health.d.ts +34 -0
- package/dist/utils/client-health.js +96 -3
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -20,9 +20,21 @@ export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
|
20
20
|
*/
|
|
21
21
|
export declare const HQ_PRO_RESOURCE_SURFACES: readonly ["api", "bot_invite", "bot_join_now", "calendar_auto_schedule", "calendar_preferences", "cli_secrets_set", "console_secrets_form", "deploy_precheck", "external_connection", "factory_install", "integrations_connect", "oauth_callback", "plan_limit_check", "secrets_input_link", "web"];
|
|
22
22
|
export declare const CLI_SECRETS_SET_SURFACE = "cli_secrets_set";
|
|
23
|
+
/**
|
|
24
|
+
* Shown when `secrets share` / `unshare` gets a 403. Changing a secret's
|
|
25
|
+
* grants needs admin on that secret, which is a separate check from read.
|
|
26
|
+
* The old "Not authorized to share this secret" text led people and agents to
|
|
27
|
+
* conclude the secret itself was locked to them, so this names the missing
|
|
28
|
+
* permission and says read access is unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SECRET_SHARE_ADMIN_REQUIRED_MESSAGE: string;
|
|
23
31
|
interface SecretAclPrincipal {
|
|
24
32
|
granteeType: "email" | "group" | "company-wide" | "person";
|
|
25
33
|
granteeId: string;
|
|
34
|
+
/** What the caller typed, when it differs from the wire grantee (agt_… uids). */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** The fleet agent behind an agt_… uid or its machine email. */
|
|
37
|
+
agentUid?: string;
|
|
26
38
|
}
|
|
27
39
|
/**
|
|
28
40
|
* hq-pro refuses any Outpost grant above read with 400
|
|
@@ -31,6 +43,15 @@ interface SecretAclPrincipal {
|
|
|
31
43
|
export declare const OUTPOST_GRANT_READ_ONLY_CODE = "OUTPOST_GRANT_READ_ONLY";
|
|
32
44
|
export declare const OUTPOST_GRANT_READ_ONLY_MESSAGE = "Company Outposts can only be given read access to a secret.";
|
|
33
45
|
export declare function parseSecretAclPrincipal(principal: string): SecretAclPrincipal | null;
|
|
46
|
+
/**
|
|
47
|
+
* Refuse a share to a fleet agent that is not an active agent of this
|
|
48
|
+
* company. hq-pro turns the machine-email grantee into an agt_… grant only
|
|
49
|
+
* for an active member; for any other agent it stores an email row the agent
|
|
50
|
+
* can never use and still returns 200. Reads the company roster
|
|
51
|
+
* (`GET /v1/agents?companyUid=…`, the same call as `hq agents list`) before
|
|
52
|
+
* any write. Returns an error message, or null when the agent is active here.
|
|
53
|
+
*/
|
|
54
|
+
export declare function inactiveCompanyAgentReason(token: string, companyUid: string, agentUid: string): Promise<string | null>;
|
|
34
55
|
export interface SecretInjectionRecipe {
|
|
35
56
|
header: string;
|
|
36
57
|
scheme: "raw" | "bearer";
|
|
@@ -45,5 +66,21 @@ export declare function parseDestinationUrl(raw: string): {
|
|
|
45
66
|
};
|
|
46
67
|
export declare function collectSecretNames(value: string, previous?: string[]): string[];
|
|
47
68
|
export declare function scrubSandboxOutput(text: string, secretNames?: string[]): string;
|
|
69
|
+
/**
|
|
70
|
+
* Check a `--remote-path` value and return the path to store, or the reason it
|
|
71
|
+
* can never match.
|
|
72
|
+
*
|
|
73
|
+
* An absolute path is sent exactly as typed: the server compares absolute
|
|
74
|
+
* approvals byte for byte with the path `hq secrets exec` reports. A relative
|
|
75
|
+
* path names the script from the HQ root. `\` becomes `/` and a leading `./`
|
|
76
|
+
* is dropped, then the same rules the server applies are checked: at least one
|
|
77
|
+
* folder before the file name, no `.`, `..` or empty segments, no `~`, no
|
|
78
|
+
* drive letter, and no surrounding whitespace.
|
|
79
|
+
*/
|
|
80
|
+
export declare function normalizeApprovedScriptPath(input: string): {
|
|
81
|
+
path: string;
|
|
82
|
+
} | {
|
|
83
|
+
error: string;
|
|
84
|
+
};
|
|
48
85
|
export declare function registerSecretsCommand(program: Command): void;
|
|
49
86
|
//# sourceMappingURL=secrets.d.ts.map
|
package/dist/commands/secrets.js
CHANGED
|
@@ -72,7 +72,7 @@ import { fastFlagEnabledDefaultOn } from "../native-fast-route.js";
|
|
|
72
72
|
import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
|
|
73
73
|
import { secretsExecChildExitCode } from "../utils/child-wrapper-exit.js";
|
|
74
74
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
75
|
-
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN, OUTPOST_UID_PATTERN, } from "./_patterns.js";
|
|
75
|
+
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN, OUTPOST_UID_PATTERN, AGENT_UID_PATTERN, agentMachineEmail, agentUidFromMachineEmail, } from "./_patterns.js";
|
|
76
76
|
import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
|
|
77
77
|
import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
|
|
78
78
|
import { formatApiKeyCapabilityDenial, peekHqApiKey, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
|
|
@@ -175,6 +175,18 @@ async function resolveSecretsTarget(capability, commandLabel, scopeOptions) {
|
|
|
175
175
|
aclPath: (action) => action ? `${basePath}/acl/${action}` : `${basePath}/acl`,
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Shown when `secrets share` / `unshare` gets a 403. Changing a secret's
|
|
180
|
+
* grants needs admin on that secret, which is a separate check from read.
|
|
181
|
+
* The old "Not authorized to share this secret" text led people and agents to
|
|
182
|
+
* conclude the secret itself was locked to them, so this names the missing
|
|
183
|
+
* permission and says read access is unchanged.
|
|
184
|
+
*/
|
|
185
|
+
export const SECRET_SHARE_ADMIN_REQUIRED_MESSAGE = "You need admin permission on this secret to change who can see it. " +
|
|
186
|
+
"This does not affect read access: if you could read the secret before, you still can. " +
|
|
187
|
+
"Ask a company admin to change its sharing.";
|
|
188
|
+
/** hq-pro 409 code: the path inherits access from a broader pattern ACL row. */
|
|
189
|
+
const SECRET_ACL_COVERED_BY_BROADER_ROW_CODE = "ACL_COVERED_BY_BROADER_ROW";
|
|
178
190
|
/**
|
|
179
191
|
* Failure message for a secrets call. A 403 on an API-key route may carry
|
|
180
192
|
* hq-pro's `{ granted, missing }` capability report — surface it verbatim so
|
|
@@ -303,7 +315,16 @@ export function parseSecretAclPrincipal(principal) {
|
|
|
303
315
|
return { granteeType: "company-wide", granteeId: "" };
|
|
304
316
|
}
|
|
305
317
|
if (EMAIL_PATTERN.test(p)) {
|
|
306
|
-
|
|
318
|
+
const email = p.toLowerCase();
|
|
319
|
+
if (email.endsWith(AGENT_EMAIL_DOMAIN)) {
|
|
320
|
+
const agentUid = agentUidFromMachineEmail(email);
|
|
321
|
+
if (!agentUid) {
|
|
322
|
+
console.error(chalk.red(`Invalid principal '${principal}': a fleet agent machine email must be agt-<26-character ULID>${AGENT_EMAIL_DOMAIN}`));
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return { granteeType: "email", granteeId: email, agentUid };
|
|
326
|
+
}
|
|
327
|
+
return { granteeType: "email", granteeId: email };
|
|
307
328
|
}
|
|
308
329
|
if (GROUP_ID_PATTERN.test(p)) {
|
|
309
330
|
return { granteeType: "group", granteeId: p };
|
|
@@ -313,12 +334,71 @@ export function parseSecretAclPrincipal(principal) {
|
|
|
313
334
|
// that this id is the company's active Outpost.
|
|
314
335
|
return { granteeType: "person", granteeId: p };
|
|
315
336
|
}
|
|
316
|
-
|
|
337
|
+
if (AGENT_UID_PATTERN.test(p)) {
|
|
338
|
+
// /delegate passes the agt_… uid (feedback_68a50c16). The wire grantee is
|
|
339
|
+
// the agent's machine email; hq-pro canonicalizes it onto the agt_…
|
|
340
|
+
// principal for an active member of the company. `secrets share` checks
|
|
341
|
+
// that membership first (inactiveCompanyAgentReason), because for any
|
|
342
|
+
// other agent the server would store an inert email row and return 200.
|
|
343
|
+
return {
|
|
344
|
+
granteeType: "email",
|
|
345
|
+
granteeId: agentMachineEmail(p),
|
|
346
|
+
label: p,
|
|
347
|
+
agentUid: p,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (/^agt_/i.test(p)) {
|
|
351
|
+
console.error(chalk.red(`Invalid principal '${principal}': a fleet agent uid must be agt_ followed by exactly 26 upper-case Crockford base32 characters (0-9, A-Z without I, L, O, U)`));
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, a group id matching grp_<alphanumeric, underscore, hyphen>, a fleet agent uid (agt_…) or its machine email (agt-…@agents.getindigo.ai), or a company Outpost id (out_…)`));
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const AGENT_EMAIL_DOMAIN = "@agents.getindigo.ai";
|
|
358
|
+
/** Agent membership states that can hold a secret grant. */
|
|
359
|
+
function isActiveRosterAgent(agent) {
|
|
360
|
+
if (agent.entityStatus !== undefined && agent.entityStatus !== "active") {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
if (agent.setupPhase === "deprovisioning" || agent.setupPhase === "deprovisioned") {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return !["archived", "deleted", "removed", "deprovisioned", "suspended", "inactive"].includes(String(agent.agentStatus ?? ""));
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Refuse a share to a fleet agent that is not an active agent of this
|
|
370
|
+
* company. hq-pro turns the machine-email grantee into an agt_… grant only
|
|
371
|
+
* for an active member; for any other agent it stores an email row the agent
|
|
372
|
+
* can never use and still returns 200. Reads the company roster
|
|
373
|
+
* (`GET /v1/agents?companyUid=…`, the same call as `hq agents list`) before
|
|
374
|
+
* any write. Returns an error message, or null when the agent is active here.
|
|
375
|
+
*/
|
|
376
|
+
export async function inactiveCompanyAgentReason(token, companyUid, agentUid) {
|
|
377
|
+
const res = await vaultApiFetch({
|
|
378
|
+
token,
|
|
379
|
+
path: "/v1/agents",
|
|
380
|
+
query: { companyUid },
|
|
381
|
+
});
|
|
382
|
+
if (!res.ok) {
|
|
383
|
+
const body = (await res.json().catch(() => ({})));
|
|
384
|
+
return `Could not check that ${agentUid} is an agent of this company (${res.status} ${body.message ?? body.error ?? res.statusText}). Nothing was shared.`;
|
|
385
|
+
}
|
|
386
|
+
const data = (await res.json().catch(() => ({})));
|
|
387
|
+
const agent = (data.agents ?? []).find((a) => typeof a.uid === "string" && a.uid.toUpperCase() === agentUid.toUpperCase());
|
|
388
|
+
if (!agent) {
|
|
389
|
+
return `${agentUid} is not an agent of this company. Nothing was shared.`;
|
|
390
|
+
}
|
|
391
|
+
if (!isActiveRosterAgent(agent)) {
|
|
392
|
+
return `${agentUid} is not an active agent of this company. Nothing was shared.`;
|
|
393
|
+
}
|
|
317
394
|
return null;
|
|
318
395
|
}
|
|
319
396
|
function describeSecretAclPrincipal(principal) {
|
|
320
|
-
|
|
321
|
-
|
|
397
|
+
if (principal.granteeType === "company-wide") {
|
|
398
|
+
return "@all (entire company, including guests)";
|
|
399
|
+
}
|
|
400
|
+
return principal.label
|
|
401
|
+
? `${principal.label} (${principal.granteeId})`
|
|
322
402
|
: principal.granteeId;
|
|
323
403
|
}
|
|
324
404
|
// Mirrors hq-pro's server-authoritative KNOWN_DESTINATION_REGISTRY
|
|
@@ -462,6 +542,61 @@ function normalizePolicyRecord(secretPath, data) {
|
|
|
462
542
|
scripts,
|
|
463
543
|
};
|
|
464
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Check a `--remote-path` value and return the path to store, or the reason it
|
|
547
|
+
* can never match.
|
|
548
|
+
*
|
|
549
|
+
* An absolute path is sent exactly as typed: the server compares absolute
|
|
550
|
+
* approvals byte for byte with the path `hq secrets exec` reports. A relative
|
|
551
|
+
* path names the script from the HQ root. `\` becomes `/` and a leading `./`
|
|
552
|
+
* is dropped, then the same rules the server applies are checked: at least one
|
|
553
|
+
* folder before the file name, no `.`, `..` or empty segments, no `~`, no
|
|
554
|
+
* drive letter, and no surrounding whitespace.
|
|
555
|
+
*/
|
|
556
|
+
export function normalizeApprovedScriptPath(input) {
|
|
557
|
+
if (input.length === 0 || input.trim().length === 0) {
|
|
558
|
+
return { error: "the path is empty" };
|
|
559
|
+
}
|
|
560
|
+
if (input !== input.trim()) {
|
|
561
|
+
return { error: "the path starts or ends with whitespace" };
|
|
562
|
+
}
|
|
563
|
+
if (input.startsWith("/") || input.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(input)) {
|
|
564
|
+
return { path: input };
|
|
565
|
+
}
|
|
566
|
+
if (input.startsWith("~")) {
|
|
567
|
+
return {
|
|
568
|
+
error: "the path starts with ~, which is never expanded; give the absolute path or a path relative to the HQ root",
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
if (/^[A-Za-z]:/.test(input)) {
|
|
572
|
+
return { error: "the path starts with a drive letter but is not absolute" };
|
|
573
|
+
}
|
|
574
|
+
const relative = input.replace(/\\/g, "/").replace(/^(\.\/)+/, "");
|
|
575
|
+
const segments = relative.split("/");
|
|
576
|
+
if (segments.some((segment) => segment === "")) {
|
|
577
|
+
return {
|
|
578
|
+
error: "the path has an empty segment (a trailing or doubled /)",
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
582
|
+
return { error: "the path has a . or .. segment" };
|
|
583
|
+
}
|
|
584
|
+
if (segments.length < 2) {
|
|
585
|
+
return {
|
|
586
|
+
error: "a bare file name would match that file in any folder; include at least one folder, for example skills/<skill>/<file>",
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return { path: relative };
|
|
590
|
+
}
|
|
591
|
+
function approvalIsActive(policy, scriptId) {
|
|
592
|
+
const approved = policy.scriptLock?.approvedScripts;
|
|
593
|
+
if (!Array.isArray(approved))
|
|
594
|
+
return true;
|
|
595
|
+
return approved.some((script) => script !== null &&
|
|
596
|
+
typeof script === "object" &&
|
|
597
|
+
script.scriptId === scriptId &&
|
|
598
|
+
!script.revokedAt);
|
|
599
|
+
}
|
|
465
600
|
function isSecretPolicyScript(value) {
|
|
466
601
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
467
602
|
return false;
|
|
@@ -985,11 +1120,12 @@ export function registerSecretsCommand(program) {
|
|
|
985
1120
|
.description("Manage approved scripts for script-locked secrets");
|
|
986
1121
|
script
|
|
987
1122
|
.command("approve <path>")
|
|
988
|
-
.description("Approve a script for a secret path")
|
|
1123
|
+
.description("Approve a script for a secret path. Each --id holds one approval: approving an id that is already approved for a different path or hash fails unless --replace is given. To let the same script run from several machines, approve one HQ-relative --remote-path instead of one absolute path per machine.")
|
|
989
1124
|
.requiredOption("--id <scriptId>", "Stable script identifier")
|
|
990
1125
|
.requiredOption("--script <path>", "Path to the local script file")
|
|
991
|
-
.option("--remote-path <path>", "Script path
|
|
1126
|
+
.option("--remote-path <path>", "Script path to approve instead of the local path. A path relative to the HQ root (for example companies/<co>/skills/<skill>/<file>) matches on every HQ root; an absolute path must match the runtime's path exactly")
|
|
992
1127
|
.option("--attestation <level>", "Attestation level", "self-asserted-hash")
|
|
1128
|
+
.option("--replace", "Replace the approval already stored under this --id (a different path or hash). Without it the command fails and keeps the existing approval")
|
|
993
1129
|
.action(async (secretPath, opts) => {
|
|
994
1130
|
try {
|
|
995
1131
|
rejectIfPersonal(secrets.opts(), "script approve");
|
|
@@ -997,7 +1133,16 @@ export function registerSecretsCommand(program) {
|
|
|
997
1133
|
console.error(chalk.red(`Invalid secret path '${secretPath}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
998
1134
|
process.exit(1);
|
|
999
1135
|
}
|
|
1136
|
+
const checkedRemotePath = opts.remotePath === undefined
|
|
1137
|
+
? undefined
|
|
1138
|
+
: normalizeApprovedScriptPath(opts.remotePath);
|
|
1139
|
+
if (checkedRemotePath && "error" in checkedRemotePath) {
|
|
1140
|
+
console.error(chalk.red(`Invalid --remote-path '${opts.remotePath}': ${checkedRemotePath.error}.`));
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
const remotePath = checkedRemotePath?.path;
|
|
1000
1144
|
const usage = await buildSecretUsage("exec", opts.script, opts.id, opts.attestation);
|
|
1145
|
+
const approvedPath = remotePath ?? usage.script?.path;
|
|
1001
1146
|
const token = await requireCognitoTokenForSecrets("secrets script approve", "secrets:write");
|
|
1002
1147
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1003
1148
|
const res = await vaultApiFetch({
|
|
@@ -1007,18 +1152,32 @@ export function registerSecretsCommand(program) {
|
|
|
1007
1152
|
body: {
|
|
1008
1153
|
path: secretPath,
|
|
1009
1154
|
scriptId: usage.script?.scriptId,
|
|
1010
|
-
scriptPath:
|
|
1155
|
+
scriptPath: approvedPath,
|
|
1011
1156
|
sha256: usage.script?.sha256,
|
|
1012
1157
|
attestationLevel: usage.script?.attestationLevel,
|
|
1158
|
+
...(opts.replace ? { replace: true } : {}),
|
|
1013
1159
|
},
|
|
1014
1160
|
});
|
|
1015
1161
|
if (!res.ok) {
|
|
1016
1162
|
const body = (await res.json().catch(() => ({})));
|
|
1163
|
+
if (res.status === 409 && body.code === "script_id_exists") {
|
|
1164
|
+
// The server kept the existing approval. Say so, and show how to
|
|
1165
|
+
// proceed, instead of a bare "Failed to approve script".
|
|
1166
|
+
const existing = body.existing;
|
|
1167
|
+
console.error(chalk.red(`Script id '${opts.id}' is already approved for '${secretPath}'${existing?.scriptPath ? ` with path ${existing.scriptPath}` : ""}. The existing approval was kept.`));
|
|
1168
|
+
console.error(`Re-run with --replace to replace it, approve this script under a new --id, or approve one HQ-relative --remote-path (for example companies/<co>/skills/<skill>/<file>) so one approval matches on every machine.`);
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1017
1171
|
console.error(chalk.red(`Failed to approve script: ${extractApiMessage(body, res.statusText)}`));
|
|
1018
1172
|
process.exit(1);
|
|
1019
1173
|
}
|
|
1020
1174
|
removeCacheEntry(companyUid, secretPath);
|
|
1021
|
-
|
|
1175
|
+
const data = (await res.json().catch(() => ({})));
|
|
1176
|
+
if (data.policy && !approvalIsActive(data.policy, opts.id)) {
|
|
1177
|
+
console.error(chalk.red(`Failed to approve script: the server did not store an active approval for '${opts.id}' on '${secretPath}'. If this id was revoked before, approve the script under a new --id and pass that id to --script-id.`));
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
console.log(chalk.green(`Approved script '${opts.id}' for '${secretPath}' (path: ${approvedPath})${opts.replace ? ", replacing the previous approval under that id" : ""}.`));
|
|
1022
1181
|
}
|
|
1023
1182
|
catch (err) {
|
|
1024
1183
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -1413,8 +1572,8 @@ export function registerSecretsCommand(program) {
|
|
|
1413
1572
|
});
|
|
1414
1573
|
secrets
|
|
1415
1574
|
.command("share <path>")
|
|
1416
|
-
.description("Share a secret with an email address, group, company Outpost (out_…), or the entire company (@all)")
|
|
1417
|
-
.requiredOption("--with <principal>", "Email address, group id, out_ Outpost id, or @all (entire company) to share with")
|
|
1575
|
+
.description("Share a secret with an email address, group, fleet agent (agt_…), company Outpost (out_…), or the entire company (@all)")
|
|
1576
|
+
.requiredOption("--with <principal>", "Email address, group id, agt_ fleet agent uid, out_ Outpost id, or @all (entire company) to share with")
|
|
1418
1577
|
.option("--permission <level>", "Permission level: read | write | admin (required, except for a company Outpost, which can only be given read and defaults to it)")
|
|
1419
1578
|
.action(async (path, opts) => {
|
|
1420
1579
|
try {
|
|
@@ -1443,6 +1602,13 @@ export function registerSecretsCommand(program) {
|
|
|
1443
1602
|
process.exit(1);
|
|
1444
1603
|
}
|
|
1445
1604
|
const target = await resolveSecretsTarget("secrets:share", "secrets share", secrets.opts());
|
|
1605
|
+
if (principal.agentUid) {
|
|
1606
|
+
const reason = await inactiveCompanyAgentReason(target.token, target.companyUid, principal.agentUid);
|
|
1607
|
+
if (reason) {
|
|
1608
|
+
console.error(chalk.red(reason));
|
|
1609
|
+
process.exit(1);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1446
1612
|
const res = await vaultApiFetch({
|
|
1447
1613
|
token: target.token,
|
|
1448
1614
|
path: target.aclPath("grant"),
|
|
@@ -1464,12 +1630,15 @@ export function registerSecretsCommand(program) {
|
|
|
1464
1630
|
}
|
|
1465
1631
|
else if (res.status === 403) {
|
|
1466
1632
|
console.error(chalk.red(target.isApiKey
|
|
1467
|
-
? formatApiKeyCapabilityDenial(body,
|
|
1468
|
-
:
|
|
1633
|
+
? formatApiKeyCapabilityDenial(body, SECRET_SHARE_ADMIN_REQUIRED_MESSAGE)
|
|
1634
|
+
: SECRET_SHARE_ADMIN_REQUIRED_MESSAGE));
|
|
1469
1635
|
}
|
|
1470
1636
|
else if (res.status === 404) {
|
|
1471
1637
|
console.error(chalk.red("ACL record not found for this path — has the secret been created?"));
|
|
1472
1638
|
}
|
|
1639
|
+
else if (res.status === 409 && body.code === SECRET_ACL_COVERED_BY_BROADER_ROW_CODE) {
|
|
1640
|
+
console.error(chalk.red(body.error ?? "This secret takes its access from a broader ACL. Share that path instead."));
|
|
1641
|
+
}
|
|
1473
1642
|
else if (res.status === 409) {
|
|
1474
1643
|
console.error(chalk.red("Concurrent modification — please retry"));
|
|
1475
1644
|
}
|
|
@@ -1491,7 +1660,7 @@ export function registerSecretsCommand(program) {
|
|
|
1491
1660
|
secrets
|
|
1492
1661
|
.command("unshare <path>")
|
|
1493
1662
|
.description("Remove a grant from a secret")
|
|
1494
|
-
.requiredOption("--from <principal>", "Email address, group id, out_ Outpost id, or @all (entire company) to remove")
|
|
1663
|
+
.requiredOption("--from <principal>", "Email address, group id, agt_ fleet agent uid, out_ Outpost id, or @all (entire company) to remove")
|
|
1495
1664
|
.action(async (path, opts) => {
|
|
1496
1665
|
try {
|
|
1497
1666
|
rejectIfPersonal(secrets.opts(), "unshare");
|
|
@@ -1521,8 +1690,8 @@ export function registerSecretsCommand(program) {
|
|
|
1521
1690
|
}
|
|
1522
1691
|
else if (res.status === 403) {
|
|
1523
1692
|
console.error(chalk.red(target.isApiKey
|
|
1524
|
-
? formatApiKeyCapabilityDenial(body,
|
|
1525
|
-
:
|
|
1693
|
+
? formatApiKeyCapabilityDenial(body, SECRET_SHARE_ADMIN_REQUIRED_MESSAGE)
|
|
1694
|
+
: SECRET_SHARE_ADMIN_REQUIRED_MESSAGE));
|
|
1526
1695
|
}
|
|
1527
1696
|
else if (res.status === 404) {
|
|
1528
1697
|
console.log(chalk.green(`Grant already absent for '${path}' / ${describeSecretAclPrincipal(principal)}`));
|
package/dist/commands/signals.js
CHANGED
|
@@ -49,10 +49,20 @@ function readActiveCompanySlug(hqRoot) {
|
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* `--company` is accepted as a synonym for `--entity`, matching `hq meetings`
|
|
54
|
+
* and `hq integrations`. Passing both with different values is an error.
|
|
55
|
+
*/
|
|
56
|
+
function pickEntityFlag(entity, company) {
|
|
57
|
+
if (entity !== undefined && company !== undefined && entity !== company) {
|
|
58
|
+
throw new Error(`--entity '${entity}' and --company '${company}' disagree. Pass one of them.`);
|
|
59
|
+
}
|
|
60
|
+
return entity ?? company;
|
|
61
|
+
}
|
|
62
|
+
function requireEntitySlug(options, hqRoot) {
|
|
63
|
+
const slug = pickEntityFlag(options.entity, options.company) ?? readActiveCompanySlug(hqRoot);
|
|
54
64
|
if (!slug) {
|
|
55
|
-
throw new Error("No entity specified. Pass --entity <slug> or run `hq signals entities` to see your options.");
|
|
65
|
+
throw new Error("No entity specified. Pass --entity <slug> (or --company <slug>) or run `hq signals entities` to see your options.");
|
|
56
66
|
}
|
|
57
67
|
return slug;
|
|
58
68
|
}
|
|
@@ -93,7 +103,7 @@ async function runList(options) {
|
|
|
93
103
|
// Throws InvalidSignalTypeError with the list of valid types if not in enum.
|
|
94
104
|
assertSignalType(options.type);
|
|
95
105
|
const signalType = options.type;
|
|
96
|
-
const slug = requireEntitySlug(options
|
|
106
|
+
const slug = requireEntitySlug(options, options.hqRoot);
|
|
97
107
|
const limit = options.limit ? Number.parseInt(options.limit, 10) : 50;
|
|
98
108
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
99
109
|
throw new Error(`--limit must be a positive integer (got '${options.limit}')`);
|
|
@@ -136,7 +146,7 @@ async function runGet(options) {
|
|
|
136
146
|
}
|
|
137
147
|
assertSignalType(options.type);
|
|
138
148
|
const signalType = options.type;
|
|
139
|
-
const slug = requireEntitySlug(options
|
|
149
|
+
const slug = requireEntitySlug(options, options.hqRoot);
|
|
140
150
|
const format = options.format ?? defaultGetFormat();
|
|
141
151
|
const accessToken = await resolveAccessToken();
|
|
142
152
|
const vaultConfig = buildVaultConfig(accessToken);
|
|
@@ -225,6 +235,7 @@ export function registerSignalsCommand(program) {
|
|
|
225
235
|
.command("list")
|
|
226
236
|
.description("List signals of a given type for an entity")
|
|
227
237
|
.option("--entity <slug>", "Entity slug (defaults to .hq/config.json activeCompany)")
|
|
238
|
+
.option("--company <slug>", "Company slug; same as --entity")
|
|
228
239
|
.option("--type <signalType>", `Signal type: ${SIGNAL_TYPES.join(" | ")}`)
|
|
229
240
|
.option("--limit <n>", "Max entries per page (default 50)")
|
|
230
241
|
.option("--page-token <token>", "Continuation token from a prior page")
|
|
@@ -236,6 +247,7 @@ export function registerSignalsCommand(program) {
|
|
|
236
247
|
.command("get")
|
|
237
248
|
.description("Fetch a single signal by id")
|
|
238
249
|
.option("--entity <slug>", "Entity slug (defaults to .hq/config.json activeCompany)")
|
|
250
|
+
.option("--company <slug>", "Company slug; same as --entity")
|
|
239
251
|
.option("--type <signalType>", `Signal type: ${SIGNAL_TYPES.join(" | ")}`)
|
|
240
252
|
.option("--id <signalId>", "Signal id (filename minus .md)")
|
|
241
253
|
.option("--format <fmt>", "Output format: markdown | json (default: markdown for TTY, json piped)")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `hq staff listings {queue|approve|reject}` — marketplace moderation from the
|
|
4
|
+
* CLI. Thin wrappers over the vault-service moderation routes:
|
|
5
|
+
*
|
|
6
|
+
* GET /v1/moderation/queue (queue)
|
|
7
|
+
* POST /v1/moderation/listings/{id} (approve | reject, body { action, note? })
|
|
8
|
+
*
|
|
9
|
+
* Every `hq publish` lands as `pending_review` and stays off the public
|
|
10
|
+
* marketplace until a staff member decides it. Before this command the only
|
|
11
|
+
* way to decide was the GTM admin web page. The server owns authorization
|
|
12
|
+
* (`isStaffEmail` on the id_token's email claim) and the state machine
|
|
13
|
+
* (pending_review -> approved | rejected, optimistic lock, superseding older
|
|
14
|
+
* approved versions). The moderation gate reads the `email` claim, which only
|
|
15
|
+
* the id_token carries, so this file uses `ensureCognitoIdToken`.
|
|
16
|
+
*/
|
|
17
|
+
export interface QueueItem {
|
|
18
|
+
listingId: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
status?: string;
|
|
21
|
+
creatorHandle?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
slug?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
summary?: string;
|
|
26
|
+
createdAt?: string;
|
|
27
|
+
injectionScan?: {
|
|
28
|
+
flagged?: boolean;
|
|
29
|
+
flags?: unknown[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare function renderQueue(items: QueueItem[]): string;
|
|
33
|
+
export declare function runListingsQueue(): Promise<void>;
|
|
34
|
+
export interface RunDecisionParams {
|
|
35
|
+
listingId: string;
|
|
36
|
+
note?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function runApproveListing(params: RunDecisionParams): Promise<void>;
|
|
39
|
+
export declare function runRejectListing(params: RunDecisionParams): Promise<void>;
|
|
40
|
+
export declare function registerListingsCommand(staff: Command): Command;
|
|
41
|
+
//# sourceMappingURL=listings.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
|
|
3
|
+
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
4
|
+
import { callerEmailFromToken, isStaffEmail } from "./reset-test-account.js";
|
|
5
|
+
// ── Rendering ───────────────────────────────────────────────────────────────
|
|
6
|
+
function renderQueueLine(item) {
|
|
7
|
+
const pack = `${item.slug ?? item.name ?? "?"}@${item.version ?? "?"}`;
|
|
8
|
+
const creator = item.creatorHandle ? `@${item.creatorHandle}` : chalk.dim("(no handle)");
|
|
9
|
+
const flag = item.injectionScan?.flagged
|
|
10
|
+
? chalk.yellow(` flagged (${item.injectionScan.flags?.length ?? 0})`)
|
|
11
|
+
: "";
|
|
12
|
+
const when = item.createdAt ? chalk.dim(` ${item.createdAt}`) : "";
|
|
13
|
+
return ` ${item.listingId} ${pack.padEnd(28)} ${(item.type ?? "").padEnd(6)} ${creator}${when}${flag}`;
|
|
14
|
+
}
|
|
15
|
+
export function renderQueue(items) {
|
|
16
|
+
if (items.length === 0) {
|
|
17
|
+
return chalk.dim("\n(no listings waiting for review)");
|
|
18
|
+
}
|
|
19
|
+
const lines = [chalk.bold(`\nListings waiting for review (${items.length}, oldest first)`)];
|
|
20
|
+
for (const item of items)
|
|
21
|
+
lines.push(renderQueueLine(item));
|
|
22
|
+
lines.push(chalk.dim("\nApprove with `hq staff listings approve <id>`, reject with `hq staff listings reject <id> --note <reason>`."));
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
|
25
|
+
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
26
|
+
async function readBody(res) {
|
|
27
|
+
try {
|
|
28
|
+
return (await res.json());
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function renderApiError(status, body) {
|
|
35
|
+
const code = typeof body.code === "string" ? body.code : undefined;
|
|
36
|
+
const message = typeof body.error === "string" ? body.error : `request failed (${status})`;
|
|
37
|
+
return code ? `${code}: ${message}` : message;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Local staff preflight so a non-staff caller stops before the round trip.
|
|
41
|
+
* The server remains the authorization boundary.
|
|
42
|
+
*/
|
|
43
|
+
async function requireStaffIdToken() {
|
|
44
|
+
const token = await ensureCognitoIdToken();
|
|
45
|
+
const callerEmail = callerEmailFromToken(token);
|
|
46
|
+
if (!callerEmail || !isStaffEmail(callerEmail)) {
|
|
47
|
+
throw new Error("hq staff listings requires a staff HQ session (@getindigo.ai, @hqforwork.com or @vyg.ai). " +
|
|
48
|
+
"Sign in with a staff account and retry.");
|
|
49
|
+
}
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
const LISTING_ID_RE = /^lst_[0-9A-Za-z]+$/;
|
|
53
|
+
function requireListingId(listingId) {
|
|
54
|
+
const id = listingId.trim();
|
|
55
|
+
if (!LISTING_ID_RE.test(id)) {
|
|
56
|
+
throw new Error(`"${listingId}" is not a listing id (expected lst_…). Run \`hq staff listings queue\` to see ids.`);
|
|
57
|
+
}
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
// ── Runners ─────────────────────────────────────────────────────────────────
|
|
61
|
+
export async function runListingsQueue() {
|
|
62
|
+
const token = await requireStaffIdToken();
|
|
63
|
+
const res = await vaultApiFetch({ token, path: "/v1/moderation/queue", method: "GET" });
|
|
64
|
+
const parsed = await readBody(res);
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
throw new Error(renderApiError(res.status, parsed));
|
|
67
|
+
const queue = parsed.queue;
|
|
68
|
+
if (!Array.isArray(queue))
|
|
69
|
+
throw new Error("server did not return a moderation queue");
|
|
70
|
+
console.log(renderQueue(queue));
|
|
71
|
+
}
|
|
72
|
+
async function decide(action, params) {
|
|
73
|
+
const id = requireListingId(params.listingId);
|
|
74
|
+
const token = await requireStaffIdToken();
|
|
75
|
+
const body = { action };
|
|
76
|
+
if (params.note !== undefined && params.note.trim().length > 0)
|
|
77
|
+
body.note = params.note.trim();
|
|
78
|
+
const res = await vaultApiFetch({
|
|
79
|
+
token,
|
|
80
|
+
path: `/v1/moderation/listings/${encodeURIComponent(id)}`,
|
|
81
|
+
method: "POST",
|
|
82
|
+
body,
|
|
83
|
+
});
|
|
84
|
+
const parsed = await readBody(res);
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const msg = renderApiError(res.status, parsed);
|
|
87
|
+
if (res.status === 409) {
|
|
88
|
+
throw new Error(`${msg} (the listing is no longer pending, or someone decided it at the same time)`);
|
|
89
|
+
}
|
|
90
|
+
throw new Error(msg);
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
function labelOf(listing, fallbackId) {
|
|
95
|
+
if (!listing)
|
|
96
|
+
return fallbackId;
|
|
97
|
+
return `${listing.slug ?? listing.name ?? fallbackId}@${listing.version ?? "?"} (${listing.listingId ?? fallbackId})`;
|
|
98
|
+
}
|
|
99
|
+
export async function runApproveListing(params) {
|
|
100
|
+
const result = await decide("approve", params);
|
|
101
|
+
console.log(chalk.green(`\n✓ approved ${labelOf(result.listing, params.listingId)}`));
|
|
102
|
+
const superseded = result.superseded ?? [];
|
|
103
|
+
if (superseded.length > 0) {
|
|
104
|
+
console.log(chalk.dim(` superseded older versions: ${superseded.join(", ")}`));
|
|
105
|
+
}
|
|
106
|
+
console.log(chalk.dim(" It is now live in the marketplace (the public pack page refreshes within ~10 minutes)."));
|
|
107
|
+
}
|
|
108
|
+
export async function runRejectListing(params) {
|
|
109
|
+
const result = await decide("reject", params);
|
|
110
|
+
console.log(chalk.yellow(`\n✗ rejected ${labelOf(result.listing, params.listingId)}`));
|
|
111
|
+
}
|
|
112
|
+
// ── Commander wiring ────────────────────────────────────────────────────────
|
|
113
|
+
function fail(err) {
|
|
114
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
export function registerListingsCommand(staff) {
|
|
118
|
+
const group = staff
|
|
119
|
+
.command("listings")
|
|
120
|
+
.description("Review marketplace listings waiting for approval (staff only). Wraps /v1/moderation.");
|
|
121
|
+
group
|
|
122
|
+
.command("queue")
|
|
123
|
+
.description("List published packs waiting for review, oldest first.")
|
|
124
|
+
.action(async () => {
|
|
125
|
+
try {
|
|
126
|
+
await runListingsQueue();
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
fail(err);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
group
|
|
133
|
+
.command("approve")
|
|
134
|
+
.description("Approve a pending listing so it goes live (older approved versions are superseded).")
|
|
135
|
+
.argument("<listingId>", "Listing id from `hq staff listings queue` (lst_…)")
|
|
136
|
+
.option("--note <text>", "Reviewer note recorded on the listing")
|
|
137
|
+
.action(async (listingId, opts) => {
|
|
138
|
+
try {
|
|
139
|
+
await runApproveListing({ listingId, note: opts.note });
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
fail(err);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
group
|
|
146
|
+
.command("reject")
|
|
147
|
+
.description("Reject a pending listing.")
|
|
148
|
+
.argument("<listingId>", "Listing id from `hq staff listings queue` (lst_…)")
|
|
149
|
+
.requiredOption("--note <reason>", "Why it was rejected (recorded on the listing)")
|
|
150
|
+
.action(async (listingId, opts) => {
|
|
151
|
+
try {
|
|
152
|
+
await runRejectListing({ listingId, note: opts.note });
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
fail(err);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return group;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=listings.js.map
|
|
@@ -76,8 +76,9 @@ interface ExecuteResult {
|
|
|
76
76
|
* non-staff caller stops before the round trip; the server remains the
|
|
77
77
|
* authorization boundary (this file could not enforce it if it wanted to).
|
|
78
78
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
79
|
+
* The domain list matches the server's `SUPER_ADMIN_DOMAINS` (getindigo.ai,
|
|
80
|
+
* hqforwork.com, vyg.ai). Exact domain match: `foo@getindigo.ai.evil.test`
|
|
81
|
+
* and `bar@xgetindigo.ai` do not qualify.
|
|
81
82
|
*/
|
|
82
83
|
export declare function isStaffEmail(candidate: unknown): boolean;
|
|
83
84
|
/** Pull the caller email out of a locally cached id_token. */
|