@indigoai-us/hq-cli 5.77.13 → 5.78.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 +34 -0
- package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
- package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
- package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
- package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
- package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
- package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
- package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
- package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
- package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
- package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
- package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
- package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
- package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
- package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
- package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
- package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
- package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
- package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
- package/assets/scaffold/core/scripts/worktree.sh +136 -0
- package/dist/commands/agents.js +28 -12
- package/dist/commands/api-keys.js +100 -20
- package/dist/commands/core.d.ts +90 -0
- package/dist/commands/core.js +279 -0
- package/dist/commands/files-browse.d.ts +21 -0
- package/dist/commands/files-browse.js +145 -24
- package/dist/commands/files.js +33 -7
- package/dist/commands/members.js +16 -5
- package/dist/commands/pack-install.d.ts +14 -0
- package/dist/commands/pack-install.js +51 -2
- package/dist/commands/pkg-install.js +43 -0
- package/dist/commands/run.js +8 -0
- package/dist/commands/secrets.js +125 -65
- package/dist/main.js +6 -0
- package/dist/utils/api-key-command-gate.d.ts +45 -0
- package/dist/utils/api-key-command-gate.js +51 -0
- package/dist/utils/hq-roots.d.ts +91 -0
- package/dist/utils/hq-roots.js +212 -0
- package/dist/utils/resolve-vault-credential.d.ts +63 -3
- package/dist/utils/resolve-vault-credential.js +152 -6
- package/dist/utils/run-bundled-script.d.ts +66 -0
- package/dist/utils/run-bundled-script.js +80 -0
- package/dist/utils/vault-api.d.ts +21 -0
- package/dist/utils/vault-api.js +74 -3
- package/package.json +7 -1
- package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
- package/.github/workflows/ci.yml +0 -46
- package/.github/workflows/publish.yml +0 -36
- package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
- package/e2e/cli.test.ts +0 -128
- package/eslint.config.js +0 -37
- package/pnpm-workspace.yaml +0 -4
- package/scripts/chmod-bins.mjs +0 -49
- package/scripts/generate-dsn.mjs +0 -26
- package/scripts/generate-scan-packages-table.mjs +0 -113
- package/scripts/smoke-sources-signals.sh +0 -103
- package/src/bin/hq-auth-refresh.ts +0 -43
- package/src/cli-version.ts +0 -13
- package/src/commands/__fixtures__/make-tar.ts +0 -126
- package/src/commands/_patterns.ts +0 -10
- package/src/commands/add.ts +0 -75
- package/src/commands/agents.test.ts +0 -527
- package/src/commands/agents.ts +0 -923
- package/src/commands/api-keys.test.ts +0 -291
- package/src/commands/api-keys.ts +0 -382
- package/src/commands/artifact-verify.test.ts +0 -177
- package/src/commands/auth.test.ts +0 -82
- package/src/commands/auth.ts +0 -213
- package/src/commands/billing.test.ts +0 -158
- package/src/commands/billing.ts +0 -146
- package/src/commands/channels.test.ts +0 -74
- package/src/commands/channels.ts +0 -91
- package/src/commands/cloud-demote.test.ts +0 -401
- package/src/commands/cloud-demote.ts +0 -278
- package/src/commands/cloud-provision.test.ts +0 -1117
- package/src/commands/cloud-provision.ts +0 -942
- package/src/commands/cloud.pull-all.test.ts +0 -711
- package/src/commands/cloud.pull-per-company.test.ts +0 -188
- package/src/commands/cloud.push-all.test.ts +0 -485
- package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
- package/src/commands/cloud.selectors.test.ts +0 -208
- package/src/commands/cloud.status.test.ts +0 -160
- package/src/commands/cloud.test.ts +0 -41
- package/src/commands/cloud.ts +0 -2130
- package/src/commands/company.test.ts +0 -202
- package/src/commands/company.ts +0 -148
- package/src/commands/creators.test.ts +0 -60
- package/src/commands/creators.ts +0 -117
- package/src/commands/crm.test.ts +0 -179
- package/src/commands/crm.ts +0 -236
- package/src/commands/db-migrate.ts +0 -55
- package/src/commands/db-provision.ts +0 -114
- package/src/commands/db-sql.ts +0 -124
- package/src/commands/db-status.ts +0 -108
- package/src/commands/db.ts +0 -26
- package/src/commands/dm.test.ts +0 -748
- package/src/commands/dm.ts +0 -1226
- package/src/commands/feedback.test.ts +0 -413
- package/src/commands/feedback.ts +0 -169
- package/src/commands/files-browse.test.ts +0 -1249
- package/src/commands/files-browse.ts +0 -1506
- package/src/commands/files-delete.test.ts +0 -484
- package/src/commands/files-recovery.test.ts +0 -370
- package/src/commands/files.test.ts +0 -722
- package/src/commands/files.ts +0 -1446
- package/src/commands/group-grants.test.ts +0 -330
- package/src/commands/group-grants.ts +0 -455
- package/src/commands/groups.test.ts +0 -44
- package/src/commands/groups.ts +0 -368
- package/src/commands/integrations.test.ts +0 -633
- package/src/commands/integrations.ts +0 -507
- package/src/commands/list.ts +0 -85
- package/src/commands/login.ts +0 -52
- package/src/commands/logout.ts +0 -30
- package/src/commands/marketplace-install.test.ts +0 -543
- package/src/commands/marketplace-security.test.ts +0 -646
- package/src/commands/mcp-registration.test.ts +0 -2787
- package/src/commands/mcp-registration.ts +0 -2612
- package/src/commands/mcp-status.test.ts +0 -483
- package/src/commands/mcp-status.ts +0 -575
- package/src/commands/mcp-status.us011.test.ts +0 -243
- package/src/commands/meetings.test.ts +0 -522
- package/src/commands/meetings.ts +0 -789
- package/src/commands/members.test.ts +0 -1474
- package/src/commands/members.ts +0 -1181
- package/src/commands/onboard-identity-guard.test.ts +0 -99
- package/src/commands/onboard-identity-guard.ts +0 -103
- package/src/commands/onboard-join.test.ts +0 -96
- package/src/commands/onboard-join.ts +0 -101
- package/src/commands/onboard-warning.test.ts +0 -26
- package/src/commands/onboard-warning.ts +0 -12
- package/src/commands/onboard.ts +0 -267
- package/src/commands/outposts-heartbeat.test.ts +0 -299
- package/src/commands/outposts-heartbeat.ts +0 -310
- package/src/commands/outposts-replica-sync.test.ts +0 -272
- package/src/commands/outposts-self-deploy.test.ts +0 -243
- package/src/commands/outposts.test.ts +0 -1039
- package/src/commands/outposts.ts +0 -1935
- package/src/commands/pack-install-secret-authorization.test.ts +0 -115
- package/src/commands/pack-install.test.ts +0 -1469
- package/src/commands/pack-install.ts +0 -2247
- package/src/commands/pack-update-cache.test.ts +0 -149
- package/src/commands/packs.test.ts +0 -88
- package/src/commands/packs.ts +0 -622
- package/src/commands/people.test.ts +0 -807
- package/src/commands/people.ts +0 -422
- package/src/commands/pkg-install.dispatch.test.ts +0 -71
- package/src/commands/pkg-install.ts +0 -215
- package/src/commands/pkg-list.test.ts +0 -79
- package/src/commands/pkg-list.ts +0 -13
- package/src/commands/pkg-remove.ts +0 -71
- package/src/commands/pkg-update.ts +0 -189
- package/src/commands/publish.test.ts +0 -650
- package/src/commands/publish.ts +0 -580
- package/src/commands/reindex.test.ts +0 -259
- package/src/commands/reindex.ts +0 -274
- package/src/commands/rescue.test.ts +0 -39
- package/src/commands/rescue.ts +0 -210
- package/src/commands/run.env-local.test.ts +0 -88
- package/src/commands/run.test.ts +0 -45
- package/src/commands/run.ts +0 -184
- package/src/commands/safe-extract.test.ts +0 -459
- package/src/commands/safe-extract.ts +0 -444
- package/src/commands/secrets-scope.test.ts +0 -56
- package/src/commands/secrets-scope.ts +0 -32
- package/src/commands/secrets.parse-destination.test.ts +0 -38
- package/src/commands/secrets.test.ts +0 -2037
- package/src/commands/secrets.ts +0 -2240
- package/src/commands/signals.ts +0 -351
- package/src/commands/skill.test.ts +0 -770
- package/src/commands/skill.ts +0 -796
- package/src/commands/sources.ts +0 -362
- package/src/commands/sync-mode.test.ts +0 -366
- package/src/commands/sync-mode.ts +0 -387
- package/src/commands/sync-narrow.test.ts +0 -633
- package/src/commands/sync-narrow.ts +0 -555
- package/src/commands/sync.ts +0 -165
- package/src/commands/team-sync.ts +0 -628
- package/src/commands/update.ts +0 -71
- package/src/commands/whoami.ts +0 -83
- package/src/commands/workers.test.ts +0 -158
- package/src/commands/workers.ts +0 -301
- package/src/index.test.ts +0 -32
- package/src/index.ts +0 -20
- package/src/lib/db/control-plane.test.ts +0 -59
- package/src/lib/db/control-plane.ts +0 -113
- package/src/lib/db/local.test.ts +0 -81
- package/src/lib/db/local.ts +0 -148
- package/src/lib/db/migrate.test.ts +0 -133
- package/src/lib/db/migrate.ts +0 -137
- package/src/lib/db/paths.test.ts +0 -112
- package/src/lib/db/paths.ts +0 -128
- package/src/lib/db/remote-engine.test.ts +0 -44
- package/src/lib/db/remote-engine.ts +0 -148
- package/src/lib/db/remote-sql.test.ts +0 -32
- package/src/lib/db/remote-sql.ts +0 -62
- package/src/lib/db/sql.test.ts +0 -106
- package/src/lib/db/sql.ts +0 -192
- package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
- package/src/lib/local-tree-diff.test.ts +0 -301
- package/src/lib/local-tree-diff.ts +0 -362
- package/src/lib/narrow-hint-banner.test.ts +0 -235
- package/src/lib/narrow-hint-banner.ts +0 -212
- package/src/lib/plan-limit-nag.test.ts +0 -317
- package/src/lib/plan-limit-nag.ts +0 -264
- package/src/main.ts +0 -371
- package/src/node-preflight.test.ts +0 -60
- package/src/node-preflight.ts +0 -67
- package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
- package/src/outpost/session-heartbeat-guard.test.ts +0 -105
- package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
- package/src/outpost/session-heartbeat-publisher.ts +0 -186
- package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
- package/src/outpost/session-heartbeat.test.ts +0 -459
- package/src/outpost/session-heartbeat.ts +0 -877
- package/src/packaging.test.ts +0 -109
- package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
- package/src/run/discover-schemas.test.ts +0 -153
- package/src/run/discover-schemas.ts +0 -79
- package/src/run/hq-plugin.test.ts +0 -371
- package/src/run/hq-plugin.ts +0 -277
- package/src/run/varlock-shape.test.ts +0 -57
- package/src/schemas/hq-package.schema.json +0 -155
- package/src/sentry-before-send.test.ts +0 -359
- package/src/sentry-before-send.ts +0 -221
- package/src/sentry-epipe.test.ts +0 -37
- package/src/sentry-release.test.ts +0 -54
- package/src/sentry.test.ts +0 -24
- package/src/sentry.ts +0 -54
- package/src/strategies/link.ts +0 -62
- package/src/strategies/merge.ts +0 -142
- package/src/types.ts +0 -146
- package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
- package/src/utils/auth-error.test.ts +0 -40
- package/src/utils/auth-error.ts +0 -42
- package/src/utils/billing-gate.test.ts +0 -95
- package/src/utils/billing-gate.ts +0 -228
- package/src/utils/breadcrumb-buffer.ts +0 -18
- package/src/utils/cli-telemetry.test.ts +0 -153
- package/src/utils/cli-telemetry.ts +0 -61
- package/src/utils/cognito-session.machine.test.ts +0 -109
- package/src/utils/cognito-session.refresh.test.ts +0 -84
- package/src/utils/cognito-session.test.ts +0 -328
- package/src/utils/cognito-session.ts +0 -448
- package/src/utils/company-selection-error.test.ts +0 -42
- package/src/utils/company-selection-error.ts +0 -45
- package/src/utils/contribution-table.ts +0 -83
- package/src/utils/environmental-error.test.ts +0 -45
- package/src/utils/environmental-error.ts +0 -39
- package/src/utils/epipe.test.ts +0 -28
- package/src/utils/epipe.ts +0 -29
- package/src/utils/expected-cli-error.test.ts +0 -28
- package/src/utils/expected-cli-error.ts +0 -39
- package/src/utils/feedback-diagnostics.test.ts +0 -183
- package/src/utils/feedback-diagnostics.ts +0 -123
- package/src/utils/feedback-screenshots.test.ts +0 -134
- package/src/utils/feedback-screenshots.ts +0 -124
- package/src/utils/feedback-versions.test.ts +0 -98
- package/src/utils/feedback-versions.ts +0 -68
- package/src/utils/git.ts +0 -74
- package/src/utils/integrity.ts +0 -59
- package/src/utils/intercepted-process-exit.test.ts +0 -37
- package/src/utils/intercepted-process-exit.ts +0 -36
- package/src/utils/login-provider.test.ts +0 -42
- package/src/utils/login-provider.ts +0 -30
- package/src/utils/manifest.test.ts +0 -167
- package/src/utils/manifest.ts +0 -131
- package/src/utils/pack-contributions.test.ts +0 -603
- package/src/utils/pack-contributions.ts +0 -450
- package/src/utils/people.ts +0 -215
- package/src/utils/registry-client.ts +0 -204
- package/src/utils/registry.ts +0 -90
- package/src/utils/resolve-vault-credential.test.ts +0 -69
- package/src/utils/resolve-vault-credential.ts +0 -60
- package/src/utils/sandbox-runner-client.test.ts +0 -279
- package/src/utils/sandbox-runner-client.ts +0 -260
- package/src/utils/secrets-cache.ts +0 -183
- package/src/utils/sentry-identity.ts +0 -45
- package/src/utils/settle-with-timeout.test.ts +0 -21
- package/src/utils/settle-with-timeout.ts +0 -22
- package/src/utils/user-yaml-error.test.ts +0 -24
- package/src/utils/user-yaml-error.ts +0 -30
- package/src/utils/vault-api.test.ts +0 -536
- package/src/utils/vault-api.ts +0 -382
- package/src/utils/version-check.test.ts +0 -176
- package/src/utils/version-check.ts +0 -155
- package/src/utils/version-gate.test.ts +0 -1024
- package/src/utils/version-gate.ts +0 -702
- package/test/commands/db-tenant-isolation.test.ts +0 -94
- package/test/commands/db.test.ts +0 -85
- package/test/commands/signals.test.ts +0 -207
- package/test/commands/sources.test.ts +0 -231
- package/test/e2e/smoke-install-mcp.sh +0 -113
- package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
- package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
- package/test/fixtures/signals/action_item/sample.md +0 -16
- package/test/fixtures/signals/summary/sample.md +0 -12
- package/test/fixtures/sources/meetings/sample.md +0 -25
- package/test/helpers/cli-runner.ts +0 -150
- package/test/helpers/s3-list-mock.ts +0 -79
- package/test/helpers/vault-service-mock.ts +0 -295
- package/test/sources-signals/smoke.test.ts +0 -228
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -11
- package/vitest.e2e.config.ts +0 -7
|
@@ -40,8 +40,9 @@ import { Readable } from "node:stream";
|
|
|
40
40
|
import { pipeline } from "node:stream/promises";
|
|
41
41
|
import { S3Client, ListObjectsV2Command, GetObjectCommand, } from "@aws-sdk/client-s3";
|
|
42
42
|
import { VaultClient, grantPathToPrefix, } from "@indigoai-us/hq-cloud";
|
|
43
|
-
import { DEFAULT_HQ_ROOT, DEFAULT_COGNITO,
|
|
44
|
-
import {
|
|
43
|
+
import { DEFAULT_HQ_ROOT, DEFAULT_COGNITO, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
44
|
+
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
45
|
+
import { getCompanyUid, looksLikeCompanyUid, vaultApiFetch, } from "../utils/vault-api.js";
|
|
45
46
|
import { resolveCanonicalPersonUid } from "./cloud.js";
|
|
46
47
|
// ── Pure helpers ────────────────────────────────────────────────────────────
|
|
47
48
|
/**
|
|
@@ -636,6 +637,119 @@ export function createCompanyPresignClient(input) {
|
|
|
636
637
|
function makeCompanyPresignFactory(token) {
|
|
637
638
|
return ({ companyUid }) => createCompanyPresignClient({ token, companyUid });
|
|
638
639
|
}
|
|
640
|
+
// ── HQ_API_KEY vault client (keyed routes only) ──────────────────────────────
|
|
641
|
+
/** Fail-closed error the top-level handler prints without a Sentry report. */
|
|
642
|
+
function apiKeyUnsupported(message) {
|
|
643
|
+
return Object.assign(new Error(message), { expected: true });
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Bucket name handed to the orchestrators on the keyed company path.
|
|
647
|
+
*
|
|
648
|
+
* `runBrowse`/`runCat`/`runGet` require a non-empty `bucketName` and pass it as
|
|
649
|
+
* `Bucket` on each S3-shaped command, but the keyed client sends
|
|
650
|
+
* `/v1/keys/files/list` + `/v1/keys/files/presign`, which resolve the real
|
|
651
|
+
* bucket server-side from the company uid — so this value is never read.
|
|
652
|
+
*/
|
|
653
|
+
const API_KEY_BUCKET_RESOLVED_SERVER_SIDE = "resolved-server-side";
|
|
654
|
+
/**
|
|
655
|
+
* `FilesBrowseVaultClient` for a Bearer `hqk_` key.
|
|
656
|
+
*
|
|
657
|
+
* `VaultClient` talks to the Cognito-JWT routes directly — it never passes
|
|
658
|
+
* through `vaultApiFetch`, so it cannot be handed an `hqk_` token: the key
|
|
659
|
+
* would arrive at a JWT-only route and come back as a 401 that reads like the
|
|
660
|
+
* caller is logged out. Company browse/cat/get/search need only three things
|
|
661
|
+
* from the client, and each has a keyed answer:
|
|
662
|
+
*
|
|
663
|
+
* - the company uid — supplied explicitly (`--company cmp_…` or
|
|
664
|
+
* HQ_COMPANY_UID). A key is company-bound and entity slug resolution has no
|
|
665
|
+
* keyed route, so a slug is refused rather than guessed.
|
|
666
|
+
* - the bucket name — unused on this path (see the constant above).
|
|
667
|
+
* - the caller's explicit grants — `GET /v1/keys/files/grants` (files:read),
|
|
668
|
+
* so the `shared-with-you` vs `role-bypass` column stays accurate.
|
|
669
|
+
*
|
|
670
|
+
* The identity surfaces a key cannot reach (`sts.vend`/`vendSelf` for the
|
|
671
|
+
* personal vault, `entity.get`, `listMyMemberships` for the cross-company
|
|
672
|
+
* roll-up) throw a fail-closed error naming the Cognito requirement.
|
|
673
|
+
*/
|
|
674
|
+
export function createApiKeyBrowseVaultClient(token) {
|
|
675
|
+
const needsCognito = (surface) => apiKeyUnsupported(`HQ_API_KEY is set; ${surface} is not supported for API keys — hq-pro ` +
|
|
676
|
+
`serves no vault API-key route for it. Unset HQ_API_KEY to use your ` +
|
|
677
|
+
`Cognito session.`);
|
|
678
|
+
return {
|
|
679
|
+
sts: {
|
|
680
|
+
vend: () => {
|
|
681
|
+
throw needsCognito("STS credential vending");
|
|
682
|
+
},
|
|
683
|
+
vendSelf: () => {
|
|
684
|
+
throw needsCognito("the personal vault (--personal)");
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
async listMyExplicitGrants(companyUid) {
|
|
688
|
+
const res = await vaultApiFetch({
|
|
689
|
+
token,
|
|
690
|
+
path: "/v1/files/grants",
|
|
691
|
+
query: { company: companyUid },
|
|
692
|
+
});
|
|
693
|
+
if (!res.ok) {
|
|
694
|
+
const body = (await res.json().catch(() => ({})));
|
|
695
|
+
throw new Error(body.message ?? body.error ?? `files grants failed (${res.status})`);
|
|
696
|
+
}
|
|
697
|
+
const body = (await res.json());
|
|
698
|
+
return body.grants ?? [];
|
|
699
|
+
},
|
|
700
|
+
listMyMemberships: () => {
|
|
701
|
+
throw needsCognito("the cross-company roll-up");
|
|
702
|
+
},
|
|
703
|
+
entity: {
|
|
704
|
+
get: () => {
|
|
705
|
+
throw needsCognito("entity lookup by uid");
|
|
706
|
+
},
|
|
707
|
+
findInMyNamespace: async (_type, ref) => {
|
|
708
|
+
// A key cannot resolve a slug (no keyed `/entity/check-slug/me`), so the
|
|
709
|
+
// company must already be a uid. The CLI layer resolves `--company` /
|
|
710
|
+
// HQ_COMPANY_UID through `getCompanyUid`, which rejects a slug under a
|
|
711
|
+
// key — this guard covers a path-derived slug reaching us anyway.
|
|
712
|
+
if (!looksLikeCompanyUid(ref)) {
|
|
713
|
+
throw apiKeyUnsupported(`HQ_API_KEY is set; the company must be a uid (cmp_…) — resolving ` +
|
|
714
|
+
`the slug '${ref}' needs a Cognito session. Pass --company cmp_… ` +
|
|
715
|
+
`(or set HQ_COMPANY_UID) and anchor the path at ` +
|
|
716
|
+
`'companies/cmp_…/'.`);
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
uid: ref,
|
|
720
|
+
slug: ref,
|
|
721
|
+
bucketName: API_KEY_BUCKET_RESOLVED_SERVER_SIDE,
|
|
722
|
+
};
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
/** Fail-closed error for `--personal` (and the roll-up) under an API key. */
|
|
728
|
+
function personalVaultNeedsCognito() {
|
|
729
|
+
return apiKeyUnsupported(`HQ_API_KEY is set; --personal is not supported for API keys — a key is ` +
|
|
730
|
+
`company-bound and the personal vault is reached by vending STS ` +
|
|
731
|
+
`credentials, which no /v1/keys route serves. Target a company with ` +
|
|
732
|
+
`--company cmp_… instead, or unset HQ_API_KEY to use your Cognito session.`);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* The caller's credential plus the vault client that can carry it.
|
|
736
|
+
*
|
|
737
|
+
* `cognito` is undefined under HQ_API_KEY: `VaultClient` speaks only the JWT
|
|
738
|
+
* routes, so the keyed adapter stands in for the company path — and the
|
|
739
|
+
* personal path, which needs `VaultClient.entity.listByType` plus an STS vend,
|
|
740
|
+
* has nothing to run on and must fail closed at its branch.
|
|
741
|
+
*/
|
|
742
|
+
async function resolveBrowseSession() {
|
|
743
|
+
const cred = await resolveVaultCredential();
|
|
744
|
+
if (cred.kind === "api-key") {
|
|
745
|
+
return {
|
|
746
|
+
token: cred.token,
|
|
747
|
+
client: createApiKeyBrowseVaultClient(cred.token),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const cognito = new VaultClient(buildVaultConfig(cred.token));
|
|
751
|
+
return { token: cred.token, client: cognito, cognito };
|
|
752
|
+
}
|
|
639
753
|
/**
|
|
640
754
|
* Wire `hq files browse` + `hq files cat` onto an existing `files`
|
|
641
755
|
* Commander group. `registerFilesCommand` in files.ts builds the group
|
|
@@ -659,23 +773,23 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
659
773
|
if (options.personal && company) {
|
|
660
774
|
throw new Error("--personal and --company are mutually exclusive. Pick one.");
|
|
661
775
|
}
|
|
662
|
-
const accessToken = await
|
|
663
|
-
const vaultConfig = buildVaultConfig(accessToken);
|
|
664
|
-
const client = new VaultClient(vaultConfig);
|
|
776
|
+
const { token: accessToken, client, cognito } = await resolveBrowseSession();
|
|
665
777
|
if (options.personal) {
|
|
778
|
+
if (!cognito)
|
|
779
|
+
throw personalVaultNeedsCognito();
|
|
666
780
|
// Personal-vault path. Resolve the caller's canonical person
|
|
667
781
|
// entity once; the orchestrator does the bucket lookup + vend.
|
|
668
782
|
// Empty [path] → list bucket root.
|
|
669
783
|
const personalUid = await resolveCanonicalPersonUid({
|
|
670
|
-
listMyMemberships: () =>
|
|
671
|
-
listPersonEntities: () =>
|
|
784
|
+
listMyMemberships: () => cognito.listMyMemberships(),
|
|
785
|
+
listPersonEntities: () => cognito.entity.listByType("person"),
|
|
672
786
|
getEntity: async () => null,
|
|
673
787
|
});
|
|
674
788
|
const result = await runBrowse({
|
|
675
789
|
pathPrefix: pathArg ?? "",
|
|
676
790
|
personalMode: true,
|
|
677
791
|
personalUid,
|
|
678
|
-
vaultClient:
|
|
792
|
+
vaultClient: cognito,
|
|
679
793
|
s3Factory: defaultS3Factory,
|
|
680
794
|
region: DEFAULT_COGNITO.region,
|
|
681
795
|
});
|
|
@@ -746,13 +860,13 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
746
860
|
if (options.personal && company) {
|
|
747
861
|
throw new Error("--personal and --company are mutually exclusive. Pick one.");
|
|
748
862
|
}
|
|
749
|
-
const accessToken = await
|
|
750
|
-
const vaultConfig = buildVaultConfig(accessToken);
|
|
751
|
-
const client = new VaultClient(vaultConfig);
|
|
863
|
+
const { token: accessToken, client, cognito } = await resolveBrowseSession();
|
|
752
864
|
if (options.personal) {
|
|
865
|
+
if (!cognito)
|
|
866
|
+
throw personalVaultNeedsCognito();
|
|
753
867
|
const personalUid = await resolveCanonicalPersonUid({
|
|
754
|
-
listMyMemberships: () =>
|
|
755
|
-
listPersonEntities: () =>
|
|
868
|
+
listMyMemberships: () => cognito.listMyMemberships(),
|
|
869
|
+
listPersonEntities: () => cognito.entity.listByType("person"),
|
|
756
870
|
getEntity: async () => null,
|
|
757
871
|
});
|
|
758
872
|
const result = await runCat({
|
|
@@ -761,7 +875,7 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
761
875
|
hqRoot: options.hqRoot,
|
|
762
876
|
personalMode: true,
|
|
763
877
|
personalUid,
|
|
764
|
-
vaultClient:
|
|
878
|
+
vaultClient: cognito,
|
|
765
879
|
s3Factory: defaultS3Factory,
|
|
766
880
|
region: DEFAULT_COGNITO.region,
|
|
767
881
|
});
|
|
@@ -811,9 +925,16 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
811
925
|
try {
|
|
812
926
|
// `--company` is bound on the parent `files` group — read merged opts.
|
|
813
927
|
const company = command.optsWithGlobals().company;
|
|
814
|
-
const accessToken = await
|
|
815
|
-
|
|
816
|
-
|
|
928
|
+
const { token: accessToken, client, cognito } = await resolveBrowseSession();
|
|
929
|
+
// The roll-up fans out over `listMyMemberships` + `entity.get`, neither
|
|
930
|
+
// of which a key can reach. Refuse up front so the caller is told to
|
|
931
|
+
// scope the query instead of watching it fail mid-fan-out.
|
|
932
|
+
if (!cognito && !company) {
|
|
933
|
+
throw apiKeyUnsupported(`HQ_API_KEY is set; the cross-company roll-up is not supported for ` +
|
|
934
|
+
`API keys — a key is bound to one company and the roll-up needs ` +
|
|
935
|
+
`your membership graph. Scope it with --company cmp_… (or set ` +
|
|
936
|
+
`HQ_COMPANY_UID), or unset HQ_API_KEY.`);
|
|
937
|
+
}
|
|
817
938
|
let companyUid;
|
|
818
939
|
if (company) {
|
|
819
940
|
// Confirm membership + resolve UID, same early-failure pattern as
|
|
@@ -845,12 +966,13 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
845
966
|
if (options.personal && company) {
|
|
846
967
|
throw new Error("--personal and --company are mutually exclusive. Pick one.");
|
|
847
968
|
}
|
|
848
|
-
const accessToken = await
|
|
849
|
-
const client = new VaultClient(buildVaultConfig(accessToken));
|
|
969
|
+
const { token: accessToken, client, cognito } = await resolveBrowseSession();
|
|
850
970
|
if (options.personal) {
|
|
971
|
+
if (!cognito)
|
|
972
|
+
throw personalVaultNeedsCognito();
|
|
851
973
|
const personalUid = await resolveCanonicalPersonUid({
|
|
852
|
-
listMyMemberships: () =>
|
|
853
|
-
listPersonEntities: () =>
|
|
974
|
+
listMyMemberships: () => cognito.listMyMemberships(),
|
|
975
|
+
listPersonEntities: () => cognito.entity.listByType("person"),
|
|
854
976
|
getEntity: async () => null,
|
|
855
977
|
});
|
|
856
978
|
const rows = await runSearch({
|
|
@@ -858,7 +980,7 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
858
980
|
companySlug: "personal",
|
|
859
981
|
personalMode: true,
|
|
860
982
|
personalUid,
|
|
861
|
-
vaultClient:
|
|
983
|
+
vaultClient: cognito,
|
|
862
984
|
s3Factory: defaultS3Factory,
|
|
863
985
|
region: DEFAULT_COGNITO.region,
|
|
864
986
|
});
|
|
@@ -891,8 +1013,7 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
891
1013
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
892
1014
|
.action(async (pathArg, options, command) => {
|
|
893
1015
|
try {
|
|
894
|
-
const accessToken = await
|
|
895
|
-
const client = new VaultClient(buildVaultConfig(accessToken));
|
|
1016
|
+
const { token: accessToken, client } = await resolveBrowseSession();
|
|
896
1017
|
// `--company` is bound on the parent `files` group — read merged opts.
|
|
897
1018
|
const companyOpt = command.optsWithGlobals().company;
|
|
898
1019
|
const slug = companyOpt ?? parseCompanySlugFromPath(pathArg);
|
package/dist/commands/files.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import open from "open";
|
|
3
3
|
import * as readline from "node:readline";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
5
|
+
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
5
6
|
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
6
7
|
import { GROUP_ID_PATTERN, EMAIL_PATTERN, normalizeFilePrefix } from "./_patterns.js";
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -92,6 +93,31 @@ export function registerFilesCommand(program) {
|
|
|
92
93
|
.command("files")
|
|
93
94
|
.description("Manage file access controls in HQ vault")
|
|
94
95
|
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
96
|
+
// An exported HQ_API_KEY must never fall back to the caller's Cognito session
|
|
97
|
+
// (that would ignore the key's scope entirely), so every subcommand is gated.
|
|
98
|
+
//
|
|
99
|
+
// hq-pro serves keyed `/v1/keys/files/…` parallels for the LIST-shaped reads
|
|
100
|
+
// (`list`, `versions`, `versions-list`, `grants`, `tombstones`) and the
|
|
101
|
+
// presign/delete/restore writes — those subcommands work under a key. The ACL
|
|
102
|
+
// and share-session surfaces are still Cognito-only: they live on the
|
|
103
|
+
// unversioned `/files/{companyUid}/acl…` + `/share-session` routes, which have
|
|
104
|
+
// no `/v1/keys` parallel. Marking them `routeAvailable: false` fails them
|
|
105
|
+
// closed AT THE GATE with the capability named, instead of letting the key
|
|
106
|
+
// travel to a JWT-only route and come back as a login-looking 401.
|
|
107
|
+
gateApiKeyCapabilities(files, {
|
|
108
|
+
default: "files:read",
|
|
109
|
+
bySubcommand: {
|
|
110
|
+
share: { capability: "files:write", routeAvailable: false },
|
|
111
|
+
unshare: { capability: "files:write", routeAvailable: false },
|
|
112
|
+
acl: { capability: "files:read", routeAvailable: false },
|
|
113
|
+
delete: "files:write",
|
|
114
|
+
restore: "files:write",
|
|
115
|
+
// `hq files search` filters a `/v1/files/list` listing (see runSearch) —
|
|
116
|
+
// it does NOT call the content-search route, so `files:read` is the
|
|
117
|
+
// capability hq-pro will actually check on `/v1/keys/files/list`.
|
|
118
|
+
search: "files:read",
|
|
119
|
+
},
|
|
120
|
+
});
|
|
95
121
|
files
|
|
96
122
|
.command("share [paths...]")
|
|
97
123
|
.description("Share file paths. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
|
|
@@ -184,7 +210,7 @@ export function registerFilesCommand(program) {
|
|
|
184
210
|
? principal.trim().toLowerCase()
|
|
185
211
|
: principal;
|
|
186
212
|
const principalLabel = isAll ? "everyone in the company" : granteeId;
|
|
187
|
-
const token = await
|
|
213
|
+
const token = (await resolveVaultCredential()).token;
|
|
188
214
|
const companySlug = files.opts().company;
|
|
189
215
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
190
216
|
const res = await vaultApiFetch({
|
|
@@ -239,7 +265,7 @@ export function registerFilesCommand(program) {
|
|
|
239
265
|
.action(async (prefix) => {
|
|
240
266
|
try {
|
|
241
267
|
const canonicalPrefix = normalizeFilePrefix(prefix);
|
|
242
|
-
const token = await
|
|
268
|
+
const token = (await resolveVaultCredential()).token;
|
|
243
269
|
const companySlug = files.opts().company;
|
|
244
270
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
245
271
|
// `/acl/tree` carries the prefix's own row metadata (directRow) and the
|
|
@@ -477,7 +503,7 @@ async function runDirectGrant(params) {
|
|
|
477
503
|
: principal;
|
|
478
504
|
// Display label used in success messages — `@all` reads better than `""`.
|
|
479
505
|
const principalLabel = isAll ? "everyone in the company" : granteeId;
|
|
480
|
-
const token = await
|
|
506
|
+
const token = (await resolveVaultCredential()).token;
|
|
481
507
|
const companyUid = await getCompanyUid(token, params.companySlug);
|
|
482
508
|
let res = await vaultApiFetch({
|
|
483
509
|
token,
|
|
@@ -555,7 +581,7 @@ async function runShareSession(params) {
|
|
|
555
581
|
}
|
|
556
582
|
expiresInMs = parsed;
|
|
557
583
|
}
|
|
558
|
-
const token = await
|
|
584
|
+
const token = (await resolveVaultCredential()).token;
|
|
559
585
|
const companyUid = await getCompanyUid(token, params.companySlug);
|
|
560
586
|
let session;
|
|
561
587
|
try {
|
|
@@ -721,7 +747,7 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
721
747
|
console.error(chalk.red("Refusing to delete the vault root. Pass a bounded prefix (e.g. 'projects/foo/' or 'projects/foo/*') or an exact key."));
|
|
722
748
|
process.exit(1);
|
|
723
749
|
}
|
|
724
|
-
const token = await
|
|
750
|
+
const token = (await resolveVaultCredential()).token;
|
|
725
751
|
// Personal scope resolves the vault server-side from the caller's identity —
|
|
726
752
|
// no company to look up. Company scope resolves the companyUid as before.
|
|
727
753
|
const companyUid = params.personal
|
|
@@ -905,7 +931,7 @@ function formatFilesRecoveryError(err, key) {
|
|
|
905
931
|
}
|
|
906
932
|
async function resolveRecoveryScope(params) {
|
|
907
933
|
assertRecoveryScope(params.personal, params.companySlug);
|
|
908
|
-
const token = await
|
|
934
|
+
const token = (await resolveVaultCredential()).token;
|
|
909
935
|
if (params.personal) {
|
|
910
936
|
return { token, personal: true };
|
|
911
937
|
}
|
package/dist/commands/members.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
3
|
+
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
3
4
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
4
5
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
5
6
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
@@ -369,6 +370,16 @@ export function registerMembersCommand(program) {
|
|
|
369
370
|
.command("members")
|
|
370
371
|
.description("Manage company memberships and invites")
|
|
371
372
|
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
373
|
+
// Membership routes are Cognito-JWT only today; an API key must fail closed
|
|
374
|
+
// rather than fall back to the caller's session.
|
|
375
|
+
gateApiKeyCapabilities(members, {
|
|
376
|
+
default: "members:read",
|
|
377
|
+
bySubcommand: {
|
|
378
|
+
promote: "members:write",
|
|
379
|
+
invite: "members:write",
|
|
380
|
+
revoke: "members:write",
|
|
381
|
+
},
|
|
382
|
+
});
|
|
372
383
|
// Canonical role-change command. `promote` is the discoverable verb users
|
|
373
384
|
// reach for; `set-role` is kept as an alias for the older name. The route is a
|
|
374
385
|
// GENERAL role change, so the help text and success message are honest that it
|
|
@@ -386,7 +397,7 @@ export function registerMembersCommand(program) {
|
|
|
386
397
|
console.error(chalk.red(`Invalid role '${newRole}': must be one of owner, admin, member, guest`));
|
|
387
398
|
process.exit(1);
|
|
388
399
|
}
|
|
389
|
-
const token = await
|
|
400
|
+
const token = (await resolveVaultCredential()).token;
|
|
390
401
|
const companySlug = members.opts().company;
|
|
391
402
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
392
403
|
const membershipKey = await resolveRoleChangeTarget(token, companyUid, target);
|
|
@@ -425,7 +436,7 @@ export function registerMembersCommand(program) {
|
|
|
425
436
|
let companyUid;
|
|
426
437
|
let companySlug;
|
|
427
438
|
try {
|
|
428
|
-
token = await
|
|
439
|
+
token = (await resolveVaultCredential()).token;
|
|
429
440
|
companySlug = members.opts().company;
|
|
430
441
|
companyUid = await getCompanyUid(token, companySlug);
|
|
431
442
|
const callerUid = await getCallerPersonUid(token);
|
|
@@ -608,7 +619,7 @@ export function registerMembersCommand(program) {
|
|
|
608
619
|
.option("--pending", "List pending invites instead of active members")
|
|
609
620
|
.action(async (opts) => {
|
|
610
621
|
try {
|
|
611
|
-
const token = await
|
|
622
|
+
const token = (await resolveVaultCredential()).token;
|
|
612
623
|
const companySlug = members.opts().company;
|
|
613
624
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
614
625
|
if (opts.pending) {
|
|
@@ -677,7 +688,7 @@ export function registerMembersCommand(program) {
|
|
|
677
688
|
.description("Revoke a pending invite. Accepts an email, personUid, full membershipKey, or legacy inviteToken.")
|
|
678
689
|
.action(async (target) => {
|
|
679
690
|
try {
|
|
680
|
-
const token = await
|
|
691
|
+
const token = (await resolveVaultCredential()).token;
|
|
681
692
|
const companySlug = members.opts().company;
|
|
682
693
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
683
694
|
const membershipKey = resolveRevokeTargetToMembershipKey(target, companyUid);
|
|
@@ -125,6 +125,19 @@ export interface MarketplaceDeps {
|
|
|
125
125
|
download: (url: string) => Promise<Uint8Array | {
|
|
126
126
|
expired: true;
|
|
127
127
|
}>;
|
|
128
|
+
/**
|
|
129
|
+
* Record the install against the listing (`POST /v1/listings/{id}/installs`).
|
|
130
|
+
*
|
|
131
|
+
* Load-bearing under HQ_API_KEY: browse + download are PUBLIC, so this is the
|
|
132
|
+
* only call that ever presents the key — which makes it the only place hq-pro
|
|
133
|
+
* can enforce `packs:install` on the key's REAL grants. The CLI-side
|
|
134
|
+
* capability gate can check the catalog, never the presented key. Optional so
|
|
135
|
+
* test doubles written before the keyed wire keep compiling.
|
|
136
|
+
*/
|
|
137
|
+
recordInstall?: (input: {
|
|
138
|
+
listingId: string;
|
|
139
|
+
companySlug?: string;
|
|
140
|
+
}) => Promise<void>;
|
|
128
141
|
}
|
|
129
142
|
interface RawListingSummary {
|
|
130
143
|
listingId?: string;
|
|
@@ -167,6 +180,7 @@ export declare function defaultMarketplaceDeps(): MarketplaceDeps;
|
|
|
167
180
|
*/
|
|
168
181
|
export declare function fetchMarketplace(source: string, tmpDir: string, deps: MarketplaceDeps, opts?: {
|
|
169
182
|
requireSignature?: boolean;
|
|
183
|
+
company?: string;
|
|
170
184
|
}): Promise<FetchResult>;
|
|
171
185
|
export interface LatestResult {
|
|
172
186
|
transport: Transport;
|
|
@@ -49,8 +49,9 @@ import { findHqRoot } from '../utils/manifest.js';
|
|
|
49
49
|
import { readHqVersion, routeContribution, listInstalledPacks } from '../utils/pack-contributions.js';
|
|
50
50
|
import { CONTRIBUTION_TABLE, payloadFor } from '../utils/contribution-table.js';
|
|
51
51
|
import { safeExtractTarball } from './safe-extract.js';
|
|
52
|
-
import { getCompanyUid, vaultApiFetchPublic } from '../utils/vault-api.js';
|
|
52
|
+
import { getCompanyUid, vaultApiFetch, vaultApiFetchPublic, } from '../utils/vault-api.js';
|
|
53
53
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
54
|
+
import { formatApiKeyCapabilityDenial, peekHqApiKey, } from '../utils/resolve-vault-credential.js';
|
|
54
55
|
import { redactSecrets, SECRET_REDACTION, registerMcpServers, McpManifestError, } from './mcp-registration.js';
|
|
55
56
|
import { listSecretCacheScopes } from '../utils/secrets-cache.js';
|
|
56
57
|
import { loadRevealedSecrets } from './secrets.js';
|
|
@@ -375,6 +376,31 @@ export function defaultMarketplaceDeps() {
|
|
|
375
376
|
}
|
|
376
377
|
return new Uint8Array(await response.arrayBuffer());
|
|
377
378
|
},
|
|
379
|
+
recordInstall: async ({ listingId, companySlug }) => {
|
|
380
|
+
// Only the API-key path calls this. A Cognito (or logged-out) install
|
|
381
|
+
// stays on the public browse+download wire — `hq install` must keep
|
|
382
|
+
// working without a login, and forcing one here would be a regression.
|
|
383
|
+
const apiKey = peekHqApiKey();
|
|
384
|
+
if (apiKey === undefined)
|
|
385
|
+
return;
|
|
386
|
+
const res = await vaultApiFetch({
|
|
387
|
+
token: apiKey,
|
|
388
|
+
// Rewritten to POST /v1/keys/listings/{id}/installs, which hq-pro
|
|
389
|
+
// serves behind `requireCapability: 'packs:install'`.
|
|
390
|
+
path: `/v1/listings/${encodeURIComponent(listingId)}/installs`,
|
|
391
|
+
method: 'POST',
|
|
392
|
+
body: companySlug
|
|
393
|
+
? { scope: 'company', companySlug }
|
|
394
|
+
: { scope: 'personal' },
|
|
395
|
+
});
|
|
396
|
+
if (res.ok)
|
|
397
|
+
return;
|
|
398
|
+
const body = (await res.json().catch(() => ({})));
|
|
399
|
+
// 403 carries hq-pro's granted/missing capability report — render it so
|
|
400
|
+
// the operator sees what the presented key actually holds.
|
|
401
|
+
throw Object.assign(new Error(formatApiKeyCapabilityDenial(body, `HQ_API_KEY was refused for this install (HTTP ${res.status}). ` +
|
|
402
|
+
`The key needs the 'packs:install' capability.`)), { expected: true });
|
|
403
|
+
},
|
|
378
404
|
};
|
|
379
405
|
}
|
|
380
406
|
/**
|
|
@@ -389,6 +415,17 @@ export function defaultMarketplaceDeps() {
|
|
|
389
415
|
export async function fetchMarketplace(source, tmpDir, deps, opts = {}) {
|
|
390
416
|
const { slug, version } = parseMarketplaceSource(source);
|
|
391
417
|
let listing = await deps.resolveListing(slug, version);
|
|
418
|
+
// Authorize the install SERVER-side before pulling any bytes. Under
|
|
419
|
+
// HQ_API_KEY this is the only call that presents the key, so it is what
|
|
420
|
+
// actually enforces `packs:install` against the key's real grants — the
|
|
421
|
+
// listing browse + tarball download are both public and would happily serve
|
|
422
|
+
// a key that holds nothing. A no-op on the Cognito path.
|
|
423
|
+
if (deps.recordInstall) {
|
|
424
|
+
await deps.recordInstall({
|
|
425
|
+
listingId: listing.listingId,
|
|
426
|
+
...(opts.company !== undefined ? { companySlug: opts.company } : {}),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
392
429
|
// Download with a single expired-URL retry: a presigned URL can expire
|
|
393
430
|
// between resolve and download, surfacing as a raw S3 403. We re-resolve the
|
|
394
431
|
// detail by id to mint a fresh URL and retry ONCE rather than leaking the 403.
|
|
@@ -1613,6 +1650,15 @@ export function runScanPackages(hqRoot, opts = {}) {
|
|
|
1613
1650
|
}
|
|
1614
1651
|
}
|
|
1615
1652
|
export async function installPack(source, opts = {}) {
|
|
1653
|
+
// NOTE: the HQ_API_KEY fail-closed gate is NOT enforced here. `installPack`
|
|
1654
|
+
// is a shared primitive called both by the top-level `hq install` CLI route
|
|
1655
|
+
// (where Cognito-only is the right gate — checked there instead) AND by
|
|
1656
|
+
// `hq packs update` (packs.ts:runUpdate), which un-wires an existing pack's
|
|
1657
|
+
// contributions BEFORE re-installing. Throwing from inside `installPack`
|
|
1658
|
+
// would leave an update mid-flight (unwired, not reinstalled) whenever
|
|
1659
|
+
// HQ_API_KEY happened to be set. Restricting the assertion to the install
|
|
1660
|
+
// entrypoint keeps `hq packs update` fail-closed at its own call site
|
|
1661
|
+
// instead (checked before it un-wires anything).
|
|
1616
1662
|
const transport = classify(source);
|
|
1617
1663
|
const hqRoot = findHqRoot();
|
|
1618
1664
|
// `readHqVersion` (shared with `hq packs`) reads the CANONICAL
|
|
@@ -1640,7 +1686,10 @@ export async function installPack(source, opts = {}) {
|
|
|
1640
1686
|
fetched = fetchLocal(source, tmpDir);
|
|
1641
1687
|
break;
|
|
1642
1688
|
case 'marketplace':
|
|
1643
|
-
fetched = await fetchMarketplace(source, tmpDir, opts.marketplaceDeps ?? defaultMarketplaceDeps(), {
|
|
1689
|
+
fetched = await fetchMarketplace(source, tmpDir, opts.marketplaceDeps ?? defaultMarketplaceDeps(), {
|
|
1690
|
+
requireSignature: opts.requireSignature,
|
|
1691
|
+
...(opts.company !== undefined ? { company: opts.company } : {}),
|
|
1692
|
+
});
|
|
1644
1693
|
break;
|
|
1645
1694
|
}
|
|
1646
1695
|
const pkg = validateManifest(fetched.payloadDir, hqVersion);
|
|
@@ -23,7 +23,41 @@ import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
|
|
|
23
23
|
import { getRegistryUrl, RegistryClient, } from '../utils/registry-client.js';
|
|
24
24
|
import { verifySha256, verifyRsaSignature } from '../utils/integrity.js';
|
|
25
25
|
import { addToRegistry } from '../utils/registry.js';
|
|
26
|
+
import { peekHqApiKey, requireApiKeyCapability, } from '../utils/resolve-vault-credential.js';
|
|
26
27
|
import { MARKETPLACE_PREFIX, installPack, sourceMatchesPackPattern, } from './pack-install.js';
|
|
28
|
+
/**
|
|
29
|
+
* True when `source` installs through the marketplace — an explicit
|
|
30
|
+
* `marketplace:` source, or a bare slug (which the dispatch below rewrites into
|
|
31
|
+
* one). Every other shape (npm scope, git URL, local path) is fetched from npm,
|
|
32
|
+
* GitHub, or the filesystem.
|
|
33
|
+
*/
|
|
34
|
+
function isMarketplaceInstall(source) {
|
|
35
|
+
return (source.startsWith(MARKETPLACE_PREFIX) || !sourceMatchesPackPattern(source));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Refuse an `HQ_API_KEY` install whose transport never presents the key.
|
|
39
|
+
*
|
|
40
|
+
* `requireApiKeyCapability` can only confirm that hq-cli KNOWS `packs:install`;
|
|
41
|
+
* the key's real grants live on the server. The marketplace transport gets them
|
|
42
|
+
* checked, because it records the install at
|
|
43
|
+
* `POST /v1/keys/listings/{id}/installs` (see `MarketplaceDeps.recordInstall`).
|
|
44
|
+
* An npm/git/local source talks to npm, GitHub, or the filesystem and never
|
|
45
|
+
* reaches hq-pro, so a key holding no capabilities at all would install exactly
|
|
46
|
+
* as well as a fully scoped one — the gate would be theatre. Fail closed and
|
|
47
|
+
* name the two ways forward.
|
|
48
|
+
*/
|
|
49
|
+
function assertApiKeyCanAuthorizeInstall(source) {
|
|
50
|
+
if (peekHqApiKey() === undefined)
|
|
51
|
+
return;
|
|
52
|
+
if (isMarketplaceInstall(source))
|
|
53
|
+
return;
|
|
54
|
+
throw Object.assign(new Error(`HQ_API_KEY is set; 'hq install ${source}' is not supported for API keys ` +
|
|
55
|
+
`— this source is fetched directly from npm/git/disk, so hq-pro never ` +
|
|
56
|
+
`sees the key and cannot enforce 'packs:install'.\n` +
|
|
57
|
+
` Install from the marketplace instead (a bare slug or ` +
|
|
58
|
+
`'marketplace:<slug>'), where the key IS authorized server-side.\n` +
|
|
59
|
+
` Unset HQ_API_KEY to use your Cognito session.`), { expected: true });
|
|
60
|
+
}
|
|
27
61
|
export function registerPackageInstallCommand(parent) {
|
|
28
62
|
parent
|
|
29
63
|
.command('install <source>')
|
|
@@ -35,6 +69,15 @@ export function registerPackageInstallCommand(parent) {
|
|
|
35
69
|
.option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
|
|
36
70
|
.action(async (source, opts) => {
|
|
37
71
|
try {
|
|
72
|
+
// Gate the top-level `hq install` entrypoint only — `installPack`
|
|
73
|
+
// itself is also called by `hq packs update` (packs.ts), which must
|
|
74
|
+
// stay able to fail closed at its OWN call site (before it un-wires
|
|
75
|
+
// an existing pack) rather than mid-flight inside installPack.
|
|
76
|
+
//
|
|
77
|
+
// Capability gate: under HQ_API_KEY the install must present a key
|
|
78
|
+
// with packs:install (hq-pro serves POST /v1/keys/listings/…/installs).
|
|
79
|
+
requireApiKeyCapability('packs:install', 'hq install');
|
|
80
|
+
assertApiKeyCanAuthorizeInstall(source);
|
|
38
81
|
if (sourceMatchesPackPattern(source)) {
|
|
39
82
|
await installPack(source, {
|
|
40
83
|
company: opts.company,
|
package/dist/commands/run.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import { internal } from 'varlock';
|
|
5
5
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
6
|
+
import { apiKeyCapabilityGateMessage, peekHqApiKey, } from '../utils/resolve-vault-credential.js';
|
|
6
7
|
import { computeSha256 } from '../utils/integrity.js';
|
|
7
8
|
import { vaultApiFetch, getCompanyUid } from '../utils/vault-api.js';
|
|
8
9
|
import { discoverSchemas } from '../run/discover-schemas.js';
|
|
@@ -38,6 +39,13 @@ export function registerRunCommand(program) {
|
|
|
38
39
|
.allowUnknownOption(true)
|
|
39
40
|
.action(async (opts) => {
|
|
40
41
|
try {
|
|
42
|
+
// `hq run` batch-loads through `/secrets/{uid}/load`, which has no
|
|
43
|
+
// Bearer `hqk_` parallel route — `hq secrets exec` is the API-key path.
|
|
44
|
+
if (peekHqApiKey() !== undefined) {
|
|
45
|
+
throw new Error(`${apiKeyCapabilityGateMessage('secrets:read', 'hq run', {
|
|
46
|
+
routeAvailable: false,
|
|
47
|
+
})}\n Use \`hq secrets exec --only <NAME> -- <command>\` with this key instead.`);
|
|
48
|
+
}
|
|
41
49
|
const dashIndex = process.argv.indexOf('--');
|
|
42
50
|
const childArgs = dashIndex !== -1 ? process.argv.slice(dashIndex + 1) : [];
|
|
43
51
|
if (!opts.check && childArgs.length === 0) {
|