@indigoai-us/hq-cli 5.345.156 → 5.345.160
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 +255 -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 +535 -10
- package/dist/command-catalog.generated.js +678 -31
- 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/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/agents.js +5 -11
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- 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-provision.js +2 -2
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +157 -17
- 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 +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/db-remote-deps.js +12 -1
- 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.d.ts +24 -0
- package/dist/commands/files.js +71 -8
- package/dist/commands/integrations-connect.js +7 -3
- 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/meetings.d.ts +4 -4
- package/dist/commands/meetings.js +17 -8
- 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 +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/skill.js +6 -6
- 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/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
- package/dist/hq-cloud-sync-runner/package.json +1 -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/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- 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/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -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.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- 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/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- 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/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
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
|
|
@@ -1040,11 +1120,12 @@ export function registerSecretsCommand(program) {
|
|
|
1040
1120
|
.description("Manage approved scripts for script-locked secrets");
|
|
1041
1121
|
script
|
|
1042
1122
|
.command("approve <path>")
|
|
1043
|
-
.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.")
|
|
1044
1124
|
.requiredOption("--id <scriptId>", "Stable script identifier")
|
|
1045
1125
|
.requiredOption("--script <path>", "Path to the local script file")
|
|
1046
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")
|
|
1047
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")
|
|
1048
1129
|
.action(async (secretPath, opts) => {
|
|
1049
1130
|
try {
|
|
1050
1131
|
rejectIfPersonal(secrets.opts(), "script approve");
|
|
@@ -1074,10 +1155,19 @@ export function registerSecretsCommand(program) {
|
|
|
1074
1155
|
scriptPath: approvedPath,
|
|
1075
1156
|
sha256: usage.script?.sha256,
|
|
1076
1157
|
attestationLevel: usage.script?.attestationLevel,
|
|
1158
|
+
...(opts.replace ? { replace: true } : {}),
|
|
1077
1159
|
},
|
|
1078
1160
|
});
|
|
1079
1161
|
if (!res.ok) {
|
|
1080
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
|
+
}
|
|
1081
1171
|
console.error(chalk.red(`Failed to approve script: ${extractApiMessage(body, res.statusText)}`));
|
|
1082
1172
|
process.exit(1);
|
|
1083
1173
|
}
|
|
@@ -1087,7 +1177,7 @@ export function registerSecretsCommand(program) {
|
|
|
1087
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.`));
|
|
1088
1178
|
process.exit(1);
|
|
1089
1179
|
}
|
|
1090
|
-
console.log(chalk.green(`Approved script '${opts.id}' for '${secretPath}' (path: ${approvedPath}).`));
|
|
1180
|
+
console.log(chalk.green(`Approved script '${opts.id}' for '${secretPath}' (path: ${approvedPath})${opts.replace ? ", replacing the previous approval under that id" : ""}.`));
|
|
1091
1181
|
}
|
|
1092
1182
|
catch (err) {
|
|
1093
1183
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -1482,8 +1572,8 @@ export function registerSecretsCommand(program) {
|
|
|
1482
1572
|
});
|
|
1483
1573
|
secrets
|
|
1484
1574
|
.command("share <path>")
|
|
1485
|
-
.description("Share a secret with an email address, group, company Outpost (out_…), or the entire company (@all)")
|
|
1486
|
-
.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")
|
|
1487
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)")
|
|
1488
1578
|
.action(async (path, opts) => {
|
|
1489
1579
|
try {
|
|
@@ -1512,6 +1602,13 @@ export function registerSecretsCommand(program) {
|
|
|
1512
1602
|
process.exit(1);
|
|
1513
1603
|
}
|
|
1514
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
|
+
}
|
|
1515
1612
|
const res = await vaultApiFetch({
|
|
1516
1613
|
token: target.token,
|
|
1517
1614
|
path: target.aclPath("grant"),
|
|
@@ -1533,12 +1630,15 @@ export function registerSecretsCommand(program) {
|
|
|
1533
1630
|
}
|
|
1534
1631
|
else if (res.status === 403) {
|
|
1535
1632
|
console.error(chalk.red(target.isApiKey
|
|
1536
|
-
? formatApiKeyCapabilityDenial(body,
|
|
1537
|
-
:
|
|
1633
|
+
? formatApiKeyCapabilityDenial(body, SECRET_SHARE_ADMIN_REQUIRED_MESSAGE)
|
|
1634
|
+
: SECRET_SHARE_ADMIN_REQUIRED_MESSAGE));
|
|
1538
1635
|
}
|
|
1539
1636
|
else if (res.status === 404) {
|
|
1540
1637
|
console.error(chalk.red("ACL record not found for this path — has the secret been created?"));
|
|
1541
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
|
+
}
|
|
1542
1642
|
else if (res.status === 409) {
|
|
1543
1643
|
console.error(chalk.red("Concurrent modification — please retry"));
|
|
1544
1644
|
}
|
|
@@ -1560,7 +1660,7 @@ export function registerSecretsCommand(program) {
|
|
|
1560
1660
|
secrets
|
|
1561
1661
|
.command("unshare <path>")
|
|
1562
1662
|
.description("Remove a grant from a secret")
|
|
1563
|
-
.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")
|
|
1564
1664
|
.action(async (path, opts) => {
|
|
1565
1665
|
try {
|
|
1566
1666
|
rejectIfPersonal(secrets.opts(), "unshare");
|
|
@@ -1590,8 +1690,8 @@ export function registerSecretsCommand(program) {
|
|
|
1590
1690
|
}
|
|
1591
1691
|
else if (res.status === 403) {
|
|
1592
1692
|
console.error(chalk.red(target.isApiKey
|
|
1593
|
-
? formatApiKeyCapabilityDenial(body,
|
|
1594
|
-
:
|
|
1693
|
+
? formatApiKeyCapabilityDenial(body, SECRET_SHARE_ADMIN_REQUIRED_MESSAGE)
|
|
1694
|
+
: SECRET_SHARE_ADMIN_REQUIRED_MESSAGE));
|
|
1595
1695
|
}
|
|
1596
1696
|
else if (res.status === 404) {
|
|
1597
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)")
|
package/dist/commands/skill.js
CHANGED
|
@@ -28,7 +28,7 @@ import { AuthError } from "../utils/auth-error.js";
|
|
|
28
28
|
import { redactErrorText } from "../utils/redact-error-text.js";
|
|
29
29
|
import { stampVaultAccessDenied, vaultAccessDeniedDiagnostics, } from "../utils/vault-access-denied-error.js";
|
|
30
30
|
import { PlanGateError } from "../utils/plan-gate-error.js";
|
|
31
|
-
import { createPlanLimitRefusalTracker, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
|
|
31
|
+
import { createPlanLimitRefusalTracker, REFUSAL_FALLBACK_SENTENCE, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
|
|
32
32
|
export const SKILL_UID_PATTERN = /^skl_[A-Za-z0-9]+$/;
|
|
33
33
|
export const SKILL_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
34
34
|
const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -54,16 +54,16 @@ async function defaultSyncFile(input) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
* The upload of a stamped skill file was refused
|
|
57
|
+
* The upload of a stamped skill file was refused (new files paused). Registration
|
|
58
58
|
* and the local stamp already succeeded, so say that on stderr, then return a
|
|
59
|
-
* typed plan gate for main.ts to render (reason and
|
|
59
|
+
* typed plan gate for main.ts to render (reason and billing link) and exit
|
|
60
60
|
* non-zero. Never "Skill ready … Sync: complete".
|
|
61
61
|
*/
|
|
62
62
|
function skillUploadRefused(skillUid, filePath, companySlug, refused) {
|
|
63
|
-
console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${filePath}', but the upload was refused
|
|
63
|
+
console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${filePath}', but the upload was refused: new files are paused for this account.`));
|
|
64
64
|
return new PlanGateError("PLAN_LIMIT_EXCEEDED", {
|
|
65
|
-
reason: `${refused.message ??
|
|
66
|
-
"The skill file was not uploaded. Once
|
|
65
|
+
reason: `${refused.message ?? REFUSAL_FALLBACK_SENTENCE} ` +
|
|
66
|
+
"The skill file was not uploaded. Once uploads resume, upload it with: " +
|
|
67
67
|
`hq sync push --company ${companySlug} '${filePath}'`,
|
|
68
68
|
...(refused.upgradeUrl ? { upgradeUrl: refused.upgradeUrl } : {}),
|
|
69
69
|
});
|
|
@@ -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. */
|
|
@@ -10,8 +10,9 @@ import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
|
10
10
|
* non-staff caller stops before the round trip; the server remains the
|
|
11
11
|
* authorization boundary (this file could not enforce it if it wanted to).
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* The domain list matches the server's `SUPER_ADMIN_DOMAINS` (getindigo.ai,
|
|
14
|
+
* hqforwork.com, vyg.ai). Exact domain match: `foo@getindigo.ai.evil.test`
|
|
15
|
+
* and `bar@xgetindigo.ai` do not qualify.
|
|
15
16
|
*/
|
|
16
17
|
export function isStaffEmail(candidate) {
|
|
17
18
|
if (typeof candidate !== "string")
|
|
@@ -20,7 +21,7 @@ export function isStaffEmail(candidate) {
|
|
|
20
21
|
if (parts.length !== 2 || parts[0].length === 0)
|
|
21
22
|
return false;
|
|
22
23
|
const domain = parts[1];
|
|
23
|
-
return domain === "getindigo.ai" || domain === "hqforwork.com";
|
|
24
|
+
return domain === "getindigo.ai" || domain === "hqforwork.com" || domain === "vyg.ai";
|
|
24
25
|
}
|
|
25
26
|
/** Pull the caller email out of a locally cached id_token. */
|
|
26
27
|
export function callerEmailFromToken(token) {
|
package/dist/commands/whoami.js
CHANGED
|
@@ -123,7 +123,7 @@ export function registerWhoamiCommand(program) {
|
|
|
123
123
|
.command('whoami')
|
|
124
124
|
.description('Show the currently authenticated user')
|
|
125
125
|
.option('--json', 'Output identity metadata as JSON (never tokens)')
|
|
126
|
-
.option('--company <slug>', 'Also report this company\'s
|
|
126
|
+
.option('--company <slug>', 'Also report this company\'s billing and usage notice')
|
|
127
127
|
.action(async (opts) => {
|
|
128
128
|
try {
|
|
129
129
|
// Effective source (same as auth status / ensureCognitoToken): env
|
package/dist/hook-engine-path.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function runHookEnginePath(args) {
|
|
|
18
18
|
console.log('Usage: hq core hook-engine-path [--repair] [--for-registration]\nResolve and pin the installed native hook engine offline.\n--for-registration prints a path only when the launcher flag is enabled.');
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
const { installHookEngine, hookEngineRequired, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
|
|
21
|
+
const { installHookEngine, hookEngineRequired, removeHookEnginePointer, verifyHookEngine, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
|
|
22
22
|
if (args.includes('--for-registration')) {
|
|
23
23
|
const { launcherEnabled } = await import('./lib/hook-engine/registration.js');
|
|
24
24
|
if (!launcherEnabled()) {
|
|
@@ -27,8 +27,19 @@ export async function runHookEnginePath(args) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
const binary = installHookEngine({ repair: args.includes('--repair') });
|
|
30
|
-
if (binary)
|
|
30
|
+
if (binary) {
|
|
31
|
+
// The printed path is one that runs and reports as an hq-hook engine.
|
|
32
|
+
// A binary that cannot answer its self-check must not be bridged, so the
|
|
33
|
+
// shell pointer is removed and the command exits non-zero.
|
|
34
|
+
const problem = verifyHookEngine(binary);
|
|
35
|
+
if (problem) {
|
|
36
|
+
removeHookEnginePointer();
|
|
37
|
+
console.error(`${problem}. Existing bash hooks remain active. Repair: ${HOOK_ENGINE_REPAIR}`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
31
41
|
console.log(binary);
|
|
42
|
+
}
|
|
32
43
|
else {
|
|
33
44
|
console.error(`Native hook engine missing; ${hookEngineRequired() ? 'repair before running hooks' : 'existing bash hooks remain active'}. Repair: ${HOOK_ENGINE_REPAIR}`);
|
|
34
45
|
process.exitCode = 1;
|