@indigoai-us/hq-cli 5.77.14 → 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 +23 -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 +42 -2
- package/dist/commands/pkg-install.js +39 -2
- package/dist/commands/run.js +6 -2
- package/dist/commands/secrets.js +120 -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 -2256
- package/src/commands/pack-update-cache.test.ts +0 -149
- package/src/commands/packs-update-api-key.test.ts +0 -105
- 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 -103
- package/src/commands/pkg-install.ts +0 -221
- 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 -190
- 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 -2050
- package/src/commands/secrets.ts +0 -2249
- 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
package/src/commands/members.ts
DELETED
|
@@ -1,1181 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
-
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
5
|
-
|
|
6
|
-
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
|
-
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
|
-
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
|
|
9
|
-
/** Fleet agent machine email: agt-…@agents.getindigo.ai */
|
|
10
|
-
const AGENT_EMAIL_PATTERN =
|
|
11
|
-
/^agt-[a-z0-9]+@agents\.getindigo\.ai$/i;
|
|
12
|
-
export const VALID_ROLES = new Set(["owner", "admin", "member", "guest"]);
|
|
13
|
-
/** Roles allowed when inviting an existing fleet agent into a company. */
|
|
14
|
-
export const AGENT_INVITE_ROLES = new Set(["admin", "member"]);
|
|
15
|
-
|
|
16
|
-
export type Role = "owner" | "admin" | "member" | "guest";
|
|
17
|
-
|
|
18
|
-
export interface PendingInvite {
|
|
19
|
-
membershipKey: string;
|
|
20
|
-
personUid?: string;
|
|
21
|
-
inviteeEmail?: string;
|
|
22
|
-
companyUid: string;
|
|
23
|
-
role: string;
|
|
24
|
-
status: string;
|
|
25
|
-
inviteToken?: string;
|
|
26
|
-
invitedBy: string;
|
|
27
|
-
invitedAt: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface MyMembership {
|
|
31
|
-
membershipKey: string;
|
|
32
|
-
personUid: string;
|
|
33
|
-
companyUid: string;
|
|
34
|
-
role: string;
|
|
35
|
-
status: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* An ACTIVE member of a company, as returned by
|
|
40
|
-
* `GET /membership/company/{companyUid}`. The server filters to
|
|
41
|
-
* `status: "active"` and enriches each row with resolved person metadata
|
|
42
|
-
* (`personEmail` / `personName` / `personSlug`) when available.
|
|
43
|
-
*/
|
|
44
|
-
export interface ActiveMember {
|
|
45
|
-
membershipKey: string;
|
|
46
|
-
personUid: string;
|
|
47
|
-
companyUid: string;
|
|
48
|
-
role: string;
|
|
49
|
-
status: string;
|
|
50
|
-
personEmail?: string;
|
|
51
|
-
personName?: string;
|
|
52
|
-
personSlug?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface InviteOptions {
|
|
56
|
-
target: string;
|
|
57
|
-
role: string;
|
|
58
|
-
paths?: string;
|
|
59
|
-
/** Email-keyed invites only — secret-group ids to attach on claim. */
|
|
60
|
-
groupIds?: string[];
|
|
61
|
-
/**
|
|
62
|
-
* Opt the server-side Resend send in/out. When `true`, hq-pro renders +
|
|
63
|
-
* sends the invitation email server-side and returns `emailSent` /
|
|
64
|
-
* `emailSkipped` / `emailError`. When `false` or unset, the legacy
|
|
65
|
-
* no-email path runs and the CLI prints the manual sign-in instructions.
|
|
66
|
-
* Requires hq-pro that supports `sendEmail` (older servers ignore the
|
|
67
|
-
* field; CLI falls back to the legacy printout in that case).
|
|
68
|
-
*/
|
|
69
|
-
sendEmail?: boolean;
|
|
70
|
-
companyUid: string;
|
|
71
|
-
callerUid: string;
|
|
72
|
-
token: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Outcome of `hq members invite`. Two shapes depending on server schema:
|
|
77
|
-
*
|
|
78
|
-
* - **schemaVersion ≤ 1** — server returns a random `inviteToken` the
|
|
79
|
-
* invitee redeems via the `hq://accept/{token}` magic link. `magicLink`
|
|
80
|
-
* is populated so the caller can print or paste it.
|
|
81
|
-
* - **schemaVersion 2+** (current production) — membership row is
|
|
82
|
-
* email-keyed and authoritative. There is no token; the invitee
|
|
83
|
-
* accepts by signing into HQ with the same email. `inviteToken` +
|
|
84
|
-
* `magicLink` are both `undefined`; the caller prints sign-in
|
|
85
|
-
* instructions instead.
|
|
86
|
-
*
|
|
87
|
-
* `membership` is always populated when the server returned 2xx.
|
|
88
|
-
*
|
|
89
|
-
* Email-related fields are populated only when hq-pro performed a
|
|
90
|
-
* server-side Resend send (caller passed `sendEmail: true` AND the
|
|
91
|
-
* server supports it). Pre-resend servers omit them entirely.
|
|
92
|
-
*/
|
|
93
|
-
export interface InviteResult {
|
|
94
|
-
inviteToken?: string;
|
|
95
|
-
magicLink?: string;
|
|
96
|
-
membership: {
|
|
97
|
-
membershipKey?: string;
|
|
98
|
-
role: string;
|
|
99
|
-
status: string;
|
|
100
|
-
inviteToken?: string;
|
|
101
|
-
inviteeEmail?: string;
|
|
102
|
-
};
|
|
103
|
-
/** Resend send actually fired and accepted by Resend. */
|
|
104
|
-
emailSent?: boolean;
|
|
105
|
-
/** Resend skipped because the server has no RESEND_API_KEY configured. */
|
|
106
|
-
emailSkipped?: boolean;
|
|
107
|
-
/** Resend was attempted but failed — human-readable reason. */
|
|
108
|
-
emailError?: string;
|
|
109
|
-
/**
|
|
110
|
-
* Present when hq-pro treated the target as a fleet agent (guest invite).
|
|
111
|
-
* Always `false` on the agent path — host keeps billing.
|
|
112
|
-
*/
|
|
113
|
-
activationBilled?: boolean;
|
|
114
|
-
/** Host company uid when the server returned an agent guest-invite result. */
|
|
115
|
-
hostCompanyUid?: string;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface ResendInviteOptions {
|
|
119
|
-
inviteeEmail: string;
|
|
120
|
-
companyUid: string;
|
|
121
|
-
callerUid: string;
|
|
122
|
-
token: string;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Outcome of `hq members invite --resend`. No new membership row was
|
|
127
|
-
* created — the server's `resend: true` short-circuit just re-fires the
|
|
128
|
-
* Resend email against the existing pending row.
|
|
129
|
-
*/
|
|
130
|
-
export interface ResendInviteResult {
|
|
131
|
-
resent: true;
|
|
132
|
-
emailSent: boolean;
|
|
133
|
-
emailSkipped: boolean;
|
|
134
|
-
emailError?: string;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export interface DetectedTarget {
|
|
138
|
-
type: "email" | "person" | "agent";
|
|
139
|
-
value: string;
|
|
140
|
-
/** True when the target is a fleet agent (uid or machine email). */
|
|
141
|
-
isAgent?: boolean;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function detectTarget(target: string): DetectedTarget | null {
|
|
145
|
-
const trimmed = target.trim();
|
|
146
|
-
if (AGENT_UID_PATTERN.test(trimmed)) {
|
|
147
|
-
return { type: "agent", value: trimmed, isAgent: true };
|
|
148
|
-
}
|
|
149
|
-
if (AGENT_EMAIL_PATTERN.test(trimmed)) {
|
|
150
|
-
return {
|
|
151
|
-
type: "email",
|
|
152
|
-
value: trimmed.toLowerCase(),
|
|
153
|
-
isAgent: true,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
if (EMAIL_PATTERN.test(trimmed)) {
|
|
157
|
-
return { type: "email", value: trimmed.toLowerCase() };
|
|
158
|
-
}
|
|
159
|
-
if (PERSON_UID_PATTERN.test(trimmed)) {
|
|
160
|
-
return { type: "person", value: trimmed };
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function shortDate(iso: string): string {
|
|
166
|
-
return iso.slice(0, 10);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Resolve the caller's personUid by reading their own membership list.
|
|
171
|
-
* The server infers the JWT identity, so this returns the canonical
|
|
172
|
-
* personUid attached to the caller's active memberships.
|
|
173
|
-
*/
|
|
174
|
-
export async function getCallerPersonUid(token: string): Promise<string> {
|
|
175
|
-
const res = await vaultApiFetch({ token, path: "/membership/me" });
|
|
176
|
-
if (!res.ok) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
"Failed to resolve caller identity — run `hq login` and try again",
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
const data = (await res.json()) as { memberships: MyMembership[] };
|
|
182
|
-
const personUid = data.memberships.find((m) => m.personUid)?.personUid;
|
|
183
|
-
if (!personUid) {
|
|
184
|
-
throw new Error(
|
|
185
|
-
"Your account has no person entity yet. Run `hq onboard create-company` or accept an invite first.",
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
return personUid;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Send a `/membership/invite` request and return the magic link. */
|
|
192
|
-
export async function inviteMember(
|
|
193
|
-
options: InviteOptions,
|
|
194
|
-
): Promise<InviteResult> {
|
|
195
|
-
if (!VALID_ROLES.has(options.role)) {
|
|
196
|
-
throw new Error(
|
|
197
|
-
`Invalid role '${options.role}': must be one of owner, admin, member, guest`,
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
if (options.paths && options.role !== "guest") {
|
|
201
|
-
throw new Error(
|
|
202
|
-
"--paths is only valid with --role guest (allowedPrefixes are only meaningful for the guest role)",
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const detected = detectTarget(options.target);
|
|
207
|
-
if (!detected) {
|
|
208
|
-
throw new Error(
|
|
209
|
-
`Invalid target '${options.target}': must be an email, personUid (prs_…), agent uid (agt_…), or agent email (agt-…@agents.getindigo.ai)`,
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Fleet agents: same invite command as humans, but only member|admin.
|
|
214
|
-
// Host company keeps billing — server never double-bills on guest invite.
|
|
215
|
-
if (detected.isAgent) {
|
|
216
|
-
if (!AGENT_INVITE_ROLES.has(options.role)) {
|
|
217
|
-
throw new Error(
|
|
218
|
-
`Agents can only be invited as member or admin (got '${options.role}'). Host company keeps billing.`,
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
if (options.paths) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
"--paths / guest role is not valid for agents — invite as member or admin",
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
if (options.groupIds && options.groupIds.length > 0) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
"--groups is not supported on agent invites — share secrets after membership with `hq secrets share`",
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
options.groupIds &&
|
|
235
|
-
options.groupIds.length > 0 &&
|
|
236
|
-
(detected.type === "person" || detected.type === "agent")
|
|
237
|
-
) {
|
|
238
|
-
throw new Error(
|
|
239
|
-
"--groups is only valid on email-keyed invites (server rejects personUid + groupIds with 400)",
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const allowedPrefixes = options.paths
|
|
244
|
-
? options.paths.split(",").map((p) => p.trim()).filter(Boolean)
|
|
245
|
-
: undefined;
|
|
246
|
-
|
|
247
|
-
const body: Record<string, unknown> = {
|
|
248
|
-
companyUid: options.companyUid,
|
|
249
|
-
role: options.role,
|
|
250
|
-
invitedBy: options.callerUid,
|
|
251
|
-
};
|
|
252
|
-
if (detected.type === "email") body.inviteeEmail = detected.value;
|
|
253
|
-
else body.personUid = detected.value;
|
|
254
|
-
if (allowedPrefixes) body.allowedPrefixes = allowedPrefixes;
|
|
255
|
-
if (options.groupIds && options.groupIds.length > 0) {
|
|
256
|
-
body.groupIds = options.groupIds;
|
|
257
|
-
}
|
|
258
|
-
if (options.sendEmail === true) body.sendEmail = true;
|
|
259
|
-
|
|
260
|
-
const res = await vaultApiFetch({
|
|
261
|
-
token: options.token,
|
|
262
|
-
path: "/membership/invite",
|
|
263
|
-
method: "POST",
|
|
264
|
-
body,
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
if (!res.ok) {
|
|
268
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
269
|
-
throw new InviteHttpError(
|
|
270
|
-
res.status,
|
|
271
|
-
err.message ?? err.error ?? res.statusText,
|
|
272
|
-
err.code,
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const data = (await res.json()) as {
|
|
277
|
-
membership?: {
|
|
278
|
-
membershipKey?: string;
|
|
279
|
-
role: string;
|
|
280
|
-
status: string;
|
|
281
|
-
inviteToken?: string;
|
|
282
|
-
inviteeEmail?: string;
|
|
283
|
-
schemaVersion?: number;
|
|
284
|
-
};
|
|
285
|
-
inviteToken?: string;
|
|
286
|
-
emailSent?: boolean;
|
|
287
|
-
emailSkipped?: boolean;
|
|
288
|
-
emailError?: string;
|
|
289
|
-
activationBilled?: boolean;
|
|
290
|
-
hostCompanyUid?: string;
|
|
291
|
-
};
|
|
292
|
-
if (!data.membership) {
|
|
293
|
-
const keys = Object.keys(data ?? {}).join(", ") || "<empty>";
|
|
294
|
-
throw new Error(
|
|
295
|
-
`Invite endpoint returned 2xx with no membership row (response keys: ${keys}). ` +
|
|
296
|
-
"This is a server-side regression — file an issue.",
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
// Two server schemas in the wild:
|
|
300
|
-
// - Legacy (schemaVersion ≤ 1): response carries a random `inviteToken`
|
|
301
|
-
// the invitee redeems via `hq://accept/{token}`.
|
|
302
|
-
// - Current (schemaVersion 2+): membership row is email-keyed and
|
|
303
|
-
// authoritative — there is no token. The invitee accepts by signing
|
|
304
|
-
// into HQ with the invited email. The CLI must NOT throw here
|
|
305
|
-
// (previously did: "response did not include an invite token") — the
|
|
306
|
-
// invite IS successfully created on the server; the caller just gets
|
|
307
|
-
// undefined for inviteToken/magicLink and prints sign-in instructions.
|
|
308
|
-
const inviteToken = data.inviteToken ?? data.membership.inviteToken;
|
|
309
|
-
return {
|
|
310
|
-
...(inviteToken
|
|
311
|
-
? { inviteToken, magicLink: `hq://accept/${inviteToken}` }
|
|
312
|
-
: {}),
|
|
313
|
-
membership: data.membership,
|
|
314
|
-
// Pre-resend hq-pro versions omit these fields entirely — leave the
|
|
315
|
-
// result fields undefined so the caller's "is server-side email
|
|
316
|
-
// supported?" check works via `typeof result.emailSent === 'boolean'`.
|
|
317
|
-
...(typeof data.emailSent === "boolean" ? { emailSent: data.emailSent } : {}),
|
|
318
|
-
...(typeof data.emailSkipped === "boolean"
|
|
319
|
-
? { emailSkipped: data.emailSkipped }
|
|
320
|
-
: {}),
|
|
321
|
-
...(data.emailError ? { emailError: data.emailError } : {}),
|
|
322
|
-
...(typeof data.activationBilled === "boolean"
|
|
323
|
-
? { activationBilled: data.activationBilled }
|
|
324
|
-
: {}),
|
|
325
|
-
...(typeof data.hostCompanyUid === "string"
|
|
326
|
-
? { hostCompanyUid: data.hostCompanyUid }
|
|
327
|
-
: {}),
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Re-fire the invite email against an existing pending row without
|
|
333
|
-
* creating a new membership row. Maps to hq-pro `/membership/invite`
|
|
334
|
-
* with `{ resend: true }` (see hq-pro PR #140). Server gates on the
|
|
335
|
-
* caller's admin/owner role.
|
|
336
|
-
*
|
|
337
|
-
* Returns the email-send status. 404 here means "no pending row exists
|
|
338
|
-
* for this email+company pair — re-invite without --resend first."
|
|
339
|
-
*/
|
|
340
|
-
export async function resendInvite(
|
|
341
|
-
options: ResendInviteOptions,
|
|
342
|
-
): Promise<ResendInviteResult> {
|
|
343
|
-
const detected = detectTarget(options.inviteeEmail);
|
|
344
|
-
if (!detected || detected.type !== "email") {
|
|
345
|
-
throw new Error(
|
|
346
|
-
"--resend requires an email target (the resend path is email-keyed-row only)",
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const res = await vaultApiFetch({
|
|
351
|
-
token: options.token,
|
|
352
|
-
path: "/membership/invite",
|
|
353
|
-
method: "POST",
|
|
354
|
-
body: {
|
|
355
|
-
companyUid: options.companyUid,
|
|
356
|
-
// Server's invite handler still requires `role` even on the resend
|
|
357
|
-
// short-circuit. Pass the safe default — server ignores it on this
|
|
358
|
-
// path (the pending row already has its real role).
|
|
359
|
-
role: "member",
|
|
360
|
-
inviteeEmail: detected.value,
|
|
361
|
-
invitedBy: options.callerUid,
|
|
362
|
-
resend: true,
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
if (!res.ok) {
|
|
367
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
368
|
-
throw new InviteHttpError(
|
|
369
|
-
res.status,
|
|
370
|
-
err.message ?? err.error ?? res.statusText,
|
|
371
|
-
err.code,
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const data = (await res.json()) as {
|
|
376
|
-
resent?: boolean;
|
|
377
|
-
emailSent?: boolean;
|
|
378
|
-
emailSkipped?: boolean;
|
|
379
|
-
emailError?: string;
|
|
380
|
-
};
|
|
381
|
-
if (data.resent !== true) {
|
|
382
|
-
throw new Error(
|
|
383
|
-
"Resend endpoint returned 2xx without `resent: true` — the connected hq-pro likely doesn't support `resend: true` yet. Upgrade hq-pro or re-invite without --resend.",
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
return {
|
|
387
|
-
resent: true,
|
|
388
|
-
emailSent: data.emailSent === true,
|
|
389
|
-
emailSkipped: data.emailSkipped === true,
|
|
390
|
-
...(data.emailError ? { emailError: data.emailError } : {}),
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export class InviteHttpError extends Error {
|
|
395
|
-
constructor(
|
|
396
|
-
public status: number,
|
|
397
|
-
message: string,
|
|
398
|
-
public code?: string,
|
|
399
|
-
) {
|
|
400
|
-
super(message);
|
|
401
|
-
this.name = "InviteHttpError";
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export function formatInviteHttpError(
|
|
406
|
-
status: number,
|
|
407
|
-
fallback: string,
|
|
408
|
-
code?: string,
|
|
409
|
-
): string {
|
|
410
|
-
if (status === 401) return "Not authenticated — please run `hq login`";
|
|
411
|
-
if (status === 403) {
|
|
412
|
-
if (code === "ADMIN_ROLE_TARGET_RESTRICTED") {
|
|
413
|
-
return "Admin can only invite at role=member; ask an owner to invite at this role";
|
|
414
|
-
}
|
|
415
|
-
return "Not authorized — only admins and owners can invite members";
|
|
416
|
-
}
|
|
417
|
-
if (status === 409) {
|
|
418
|
-
return "This person already has a membership or pending invite for this company";
|
|
419
|
-
}
|
|
420
|
-
if (status >= 500) return `Server error: ${fallback}`;
|
|
421
|
-
return fallback;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export async function listPendingInvites(
|
|
425
|
-
token: string,
|
|
426
|
-
companyUid: string,
|
|
427
|
-
): Promise<PendingInvite[]> {
|
|
428
|
-
const res = await vaultApiFetch({
|
|
429
|
-
token,
|
|
430
|
-
path: `/membership/company/${encodeURIComponent(companyUid)}/pending`,
|
|
431
|
-
});
|
|
432
|
-
if (!res.ok) {
|
|
433
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
434
|
-
throw new InviteHttpError(
|
|
435
|
-
res.status,
|
|
436
|
-
err.message ?? err.error ?? res.statusText,
|
|
437
|
-
err.code,
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
// Server schema: `{ pending: [...] }`. Earlier dev branches used
|
|
441
|
-
// `{ invites: [...] }` which the CLI still accepts as a fallback for
|
|
442
|
-
// operators running staging stages that haven't caught up yet.
|
|
443
|
-
const data = (await res.json()) as {
|
|
444
|
-
pending?: PendingInvite[] | null;
|
|
445
|
-
invites?: PendingInvite[] | null;
|
|
446
|
-
};
|
|
447
|
-
return data?.pending ?? data?.invites ?? [];
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
export async function listActiveMembers(
|
|
451
|
-
token: string,
|
|
452
|
-
companyUid: string,
|
|
453
|
-
): Promise<ActiveMember[]> {
|
|
454
|
-
const res = await vaultApiFetch({
|
|
455
|
-
token,
|
|
456
|
-
path: `/membership/company/${encodeURIComponent(companyUid)}`,
|
|
457
|
-
});
|
|
458
|
-
if (!res.ok) {
|
|
459
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
460
|
-
throw new InviteHttpError(
|
|
461
|
-
res.status,
|
|
462
|
-
err.message ?? err.error ?? res.statusText,
|
|
463
|
-
err.code,
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
// Server schema: `{ members: [...] }` — active members only, enriched with
|
|
467
|
-
// resolved person metadata (personEmail / personName / personSlug).
|
|
468
|
-
const data = (await res.json()) as {
|
|
469
|
-
members?: ActiveMember[] | null;
|
|
470
|
-
};
|
|
471
|
-
return data?.members ?? [];
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Resolve a role-change target to an active membership key.
|
|
476
|
-
*
|
|
477
|
-
* Pending invites are email-keyed, but claimed active memberships are keyed by
|
|
478
|
-
* personUid. Consequently, role changes must look up email targets in the
|
|
479
|
-
* active member list instead of synthesizing an `email:...#company` key.
|
|
480
|
-
*/
|
|
481
|
-
export async function resolveRoleChangeTarget(
|
|
482
|
-
token: string,
|
|
483
|
-
companyUid: string,
|
|
484
|
-
target: string,
|
|
485
|
-
): Promise<string> {
|
|
486
|
-
if (target.includes("#")) return target;
|
|
487
|
-
|
|
488
|
-
const detected = detectTarget(target);
|
|
489
|
-
if (detected?.type === "person" || detected?.type === "agent") {
|
|
490
|
-
return `${detected.value}#${companyUid}`;
|
|
491
|
-
}
|
|
492
|
-
if (detected?.type === "email") {
|
|
493
|
-
// Active fleet-agent guest memberships are keyed agt_…#cmp_…, not by
|
|
494
|
-
// personEmail enrichment. Map machine emails the same way revoke does
|
|
495
|
-
// before falling through to the human roster lookup.
|
|
496
|
-
if (detected.isAgent) {
|
|
497
|
-
const local = detected.value.split("@")[0] ?? "";
|
|
498
|
-
const m = local.match(/^agt-(.+)$/i);
|
|
499
|
-
if (m) return `agt_${m[1].toUpperCase()}#${companyUid}`;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const members = await listActiveMembers(token, companyUid);
|
|
503
|
-
const member = members.find(
|
|
504
|
-
(candidate) =>
|
|
505
|
-
candidate.personEmail?.trim().toLowerCase() === detected.value,
|
|
506
|
-
);
|
|
507
|
-
if (member) return member.membershipKey;
|
|
508
|
-
|
|
509
|
-
throw new Error(
|
|
510
|
-
`No active member has email '${detected.value}' in this company. ` +
|
|
511
|
-
"Run `hq members list` to find the member, or use their prs_ personUid.",
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
throw new Error(
|
|
516
|
-
`Invalid target '${target}': use an email, prs_ personUid, agt_ agentUid, or full membership key`,
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
|
|
522
|
-
* the server requires. Accepts three input forms:
|
|
523
|
-
*
|
|
524
|
-
* 1. Full membership key — already has `#<companyUid>`; passed through.
|
|
525
|
-
* Examples: `email:alice@example.com#cmp_abc`, `prs_abc#cmp_abc`.
|
|
526
|
-
* 2. Bare email — wrap as `email:<email>#<companyUid>`.
|
|
527
|
-
* 3. Bare personUid (`prs_*`) — wrap as `<personUid>#<companyUid>`.
|
|
528
|
-
* 4. Anything else (e.g. legacy schemaVersion-1 inviteToken) — pass through
|
|
529
|
-
* so the server can decide. Server may 404 if the token doesn't resolve.
|
|
530
|
-
*
|
|
531
|
-
* Pure function — no I/O — so it's trivially unit-testable. The previous
|
|
532
|
-
* shape sent the user's raw arg straight through, which meant `hq members
|
|
533
|
-
* revoke alice@example.com` always 404'd ("Invite not found") even when
|
|
534
|
-
* that exact email was just shown by `hq members list`.
|
|
535
|
-
*/
|
|
536
|
-
export function resolveRevokeTargetToMembershipKey(
|
|
537
|
-
arg: string,
|
|
538
|
-
companyUid: string,
|
|
539
|
-
): string {
|
|
540
|
-
if (arg.includes("#")) return arg;
|
|
541
|
-
const detected = detectTarget(arg);
|
|
542
|
-
if (detected?.type === "email") {
|
|
543
|
-
// Active agent guest memberships are personUid-keyed (agt_…#cmp_…), not
|
|
544
|
-
// email-keyed. Map machine emails to that key so revoke works after invite.
|
|
545
|
-
if (detected.isAgent) {
|
|
546
|
-
const local = detected.value.split("@")[0] ?? "";
|
|
547
|
-
const m = local.match(/^agt-(.+)$/i);
|
|
548
|
-
if (m) return `agt_${m[1].toUpperCase()}#${companyUid}`;
|
|
549
|
-
}
|
|
550
|
-
return `email:${detected.value}#${companyUid}`;
|
|
551
|
-
}
|
|
552
|
-
if (detected?.type === "person" || detected?.type === "agent") {
|
|
553
|
-
return `${detected.value}#${companyUid}`;
|
|
554
|
-
}
|
|
555
|
-
return arg;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function printEmailStatus(opts: {
|
|
559
|
-
recipient: string;
|
|
560
|
-
sent: boolean;
|
|
561
|
-
skipped: boolean;
|
|
562
|
-
error?: string;
|
|
563
|
-
}): void {
|
|
564
|
-
if (opts.sent) {
|
|
565
|
-
console.log(
|
|
566
|
-
chalk.green(`✓ Invitation email sent to ${opts.recipient.toLowerCase()}`),
|
|
567
|
-
);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
if (opts.skipped) {
|
|
571
|
-
console.log(
|
|
572
|
-
chalk.yellow(
|
|
573
|
-
`⚠ Email send skipped — hq-pro has no RESEND_API_KEY configured for this stage.`,
|
|
574
|
-
),
|
|
575
|
-
);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
if (opts.error) {
|
|
579
|
-
console.log(chalk.yellow(`⚠ Email send failed: ${opts.error}`));
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
console.log(
|
|
583
|
-
chalk.yellow(
|
|
584
|
-
`⚠ Email status unknown — hq-pro returned no email fields (likely a pre-resend deploy).`,
|
|
585
|
-
),
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
export async function revokeInvite(
|
|
590
|
-
token: string,
|
|
591
|
-
tokenOrKey: string,
|
|
592
|
-
companyUid: string,
|
|
593
|
-
): Promise<void> {
|
|
594
|
-
const res = await vaultApiFetch({
|
|
595
|
-
token,
|
|
596
|
-
path: "/membership/revoke",
|
|
597
|
-
method: "POST",
|
|
598
|
-
body: { membershipKey: tokenOrKey, companyUid },
|
|
599
|
-
});
|
|
600
|
-
if (!res.ok) {
|
|
601
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
602
|
-
throw new InviteHttpError(
|
|
603
|
-
res.status,
|
|
604
|
-
err.message ?? err.error ?? res.statusText,
|
|
605
|
-
err.code,
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Change a member's role via `POST /membership/role`, accepting the FULL role
|
|
612
|
-
* set (owner|admin|member|guest). This is a GENERAL role change — it can promote
|
|
613
|
-
* OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
|
|
614
|
-
* promote-to-owner / change-an-owner) is enforced SERVER-side; this function
|
|
615
|
-
* only validates the role string locally and surfaces the server's error.
|
|
616
|
-
* Role string is validated BEFORE any network call so callers/tests can rely on
|
|
617
|
-
* a synchronous-shaped rejection for a bad role.
|
|
618
|
-
*/
|
|
619
|
-
export async function changeMemberRole(
|
|
620
|
-
token: string,
|
|
621
|
-
companyUid: string,
|
|
622
|
-
membershipKey: string,
|
|
623
|
-
newRole: Role,
|
|
624
|
-
): Promise<void> {
|
|
625
|
-
if (!VALID_ROLES.has(newRole)) {
|
|
626
|
-
throw new Error(
|
|
627
|
-
`Invalid role '${newRole}': must be one of owner, admin, member, guest`,
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const res = await vaultApiFetch({
|
|
632
|
-
token,
|
|
633
|
-
path: "/membership/role",
|
|
634
|
-
method: "POST",
|
|
635
|
-
body: { companyUid, membershipKey, newRole },
|
|
636
|
-
});
|
|
637
|
-
if (!res.ok) {
|
|
638
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
639
|
-
throw new InviteHttpError(
|
|
640
|
-
res.status,
|
|
641
|
-
err.message ?? err.error ?? res.statusText,
|
|
642
|
-
err.code,
|
|
643
|
-
);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
export function registerMembersCommand(program: Command): void {
|
|
648
|
-
const members = program
|
|
649
|
-
.command("members")
|
|
650
|
-
.description("Manage company memberships and invites")
|
|
651
|
-
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
652
|
-
|
|
653
|
-
// Canonical role-change command. `promote` is the discoverable verb users
|
|
654
|
-
// reach for; `set-role` is kept as an alias for the older name. The route is a
|
|
655
|
-
// GENERAL role change, so the help text and success message are honest that it
|
|
656
|
-
// can demote as well as promote. Authorization is enforced server-side.
|
|
657
|
-
members
|
|
658
|
-
.command("promote <target> <newRole>")
|
|
659
|
-
.alias("set-role")
|
|
660
|
-
.description(
|
|
661
|
-
"Change a member's role (owner|admin|member|guest) — promotes OR demotes. " +
|
|
662
|
-
"<target> may be an email, a prs_ personUid, or a full membership key. " +
|
|
663
|
-
"Owner-or-admin only; setting a target to owner (or changing an owner) is owner-only. Server-enforced.",
|
|
664
|
-
)
|
|
665
|
-
.action(async (target: string, newRole: string) => {
|
|
666
|
-
try {
|
|
667
|
-
const role = newRole.trim().toLowerCase();
|
|
668
|
-
if (!VALID_ROLES.has(role)) {
|
|
669
|
-
console.error(
|
|
670
|
-
chalk.red(
|
|
671
|
-
`Invalid role '${newRole}': must be one of owner, admin, member, guest`,
|
|
672
|
-
),
|
|
673
|
-
);
|
|
674
|
-
process.exit(1);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const token = await ensureCognitoToken();
|
|
678
|
-
const companySlug = members.opts().company as string | undefined;
|
|
679
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
680
|
-
const membershipKey = await resolveRoleChangeTarget(
|
|
681
|
-
token,
|
|
682
|
-
companyUid,
|
|
683
|
-
target,
|
|
684
|
-
);
|
|
685
|
-
|
|
686
|
-
await changeMemberRole(token, companyUid, membershipKey, role as Role);
|
|
687
|
-
console.log(
|
|
688
|
-
chalk.green(`Updated role for '${target}' to ${role}`),
|
|
689
|
-
);
|
|
690
|
-
if (role === "owner" || role === "admin") {
|
|
691
|
-
// Clarify the common confusion: owners/admins already get full
|
|
692
|
-
// access to ALL company context automatically via the role bypass
|
|
693
|
-
// (hq-pro resolveEffectivePermission) — there is no separate context
|
|
694
|
-
// grant to give them. What they SEE locally is governed by their sync
|
|
695
|
-
// mode, not by access.
|
|
696
|
-
console.log(
|
|
697
|
-
chalk.dim(
|
|
698
|
-
`${role === "owner" ? "Owners" : "Admins"} automatically have full read and write access to all of this company's context — no separate context grant is needed.\n` +
|
|
699
|
-
`What they actually see synced to their machine is governed by their sync mode (\`hq sync mode\`), not by access.`,
|
|
700
|
-
),
|
|
701
|
-
);
|
|
702
|
-
}
|
|
703
|
-
} catch (err) {
|
|
704
|
-
if (err instanceof InviteHttpError) {
|
|
705
|
-
console.error(chalk.red(err.message));
|
|
706
|
-
process.exit(1);
|
|
707
|
-
}
|
|
708
|
-
console.error(
|
|
709
|
-
chalk.red("Error:"),
|
|
710
|
-
err instanceof Error ? err.message : String(err),
|
|
711
|
-
);
|
|
712
|
-
process.exit(1);
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
members
|
|
717
|
-
.command("invite <target>")
|
|
718
|
-
.description(
|
|
719
|
-
"Invite a person or fleet agent to the company. People: email or prs_…. Agents: agt_… or agt-…@agents.getindigo.ai (member|admin; host keeps billing, no double charge).",
|
|
720
|
-
)
|
|
721
|
-
.option(
|
|
722
|
-
"--role <role>",
|
|
723
|
-
"Role: person → owner|admin|member|guest; agent → member|admin only",
|
|
724
|
-
"member",
|
|
725
|
-
)
|
|
726
|
-
.option(
|
|
727
|
-
"--paths <prefixes>",
|
|
728
|
-
"Comma-separated allowed prefixes (only valid with --role guest)",
|
|
729
|
-
)
|
|
730
|
-
.option(
|
|
731
|
-
"--groups <ids>",
|
|
732
|
-
"Comma-separated secret-group ids the invitee will be added to on claim (email-keyed invites only)",
|
|
733
|
-
)
|
|
734
|
-
.option(
|
|
735
|
-
"--send-email",
|
|
736
|
-
"Have hq-pro send an invitation email via Resend (default true for email-keyed invites). Pre-resend hq-pro versions ignore this and the legacy 'no email sent' instructions print instead.",
|
|
737
|
-
true,
|
|
738
|
-
)
|
|
739
|
-
.option(
|
|
740
|
-
"--no-send-email",
|
|
741
|
-
"Skip the server-side Resend send — only create the pending DDB row. Useful when you're scripting bulk invites and will send the announcement out-of-band.",
|
|
742
|
-
)
|
|
743
|
-
.option(
|
|
744
|
-
"--resend",
|
|
745
|
-
"Re-fire the invitation email against an existing pending row without creating a new row. Maps to hq-pro `resend: true` short-circuit. Email-keyed invites only.",
|
|
746
|
-
)
|
|
747
|
-
.action(
|
|
748
|
-
async (
|
|
749
|
-
target: string,
|
|
750
|
-
opts: {
|
|
751
|
-
role: string;
|
|
752
|
-
paths?: string;
|
|
753
|
-
groups?: string;
|
|
754
|
-
sendEmail: boolean;
|
|
755
|
-
resend?: boolean;
|
|
756
|
-
},
|
|
757
|
-
) => {
|
|
758
|
-
let token: string | undefined;
|
|
759
|
-
let companyUid: string | undefined;
|
|
760
|
-
let companySlug: string | undefined;
|
|
761
|
-
try {
|
|
762
|
-
token = await ensureCognitoToken();
|
|
763
|
-
companySlug = members.opts().company as string | undefined;
|
|
764
|
-
companyUid = await getCompanyUid(token, companySlug);
|
|
765
|
-
const callerUid = await getCallerPersonUid(token);
|
|
766
|
-
|
|
767
|
-
// --resend short-circuits to the server's re-fire path. No new row.
|
|
768
|
-
if (opts.resend === true) {
|
|
769
|
-
const resendResult = await resendInvite({
|
|
770
|
-
inviteeEmail: target,
|
|
771
|
-
companyUid,
|
|
772
|
-
callerUid,
|
|
773
|
-
token,
|
|
774
|
-
});
|
|
775
|
-
console.log(
|
|
776
|
-
chalk.green(
|
|
777
|
-
`Re-fired invite email for ${target.toLowerCase()} (existing pending row left untouched)`,
|
|
778
|
-
),
|
|
779
|
-
);
|
|
780
|
-
console.log();
|
|
781
|
-
printEmailStatus({
|
|
782
|
-
recipient: target,
|
|
783
|
-
sent: resendResult.emailSent,
|
|
784
|
-
skipped: resendResult.emailSkipped,
|
|
785
|
-
error: resendResult.emailError,
|
|
786
|
-
});
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const groupIds = opts.groups
|
|
791
|
-
? opts.groups.split(",").map((g) => g.trim()).filter(Boolean)
|
|
792
|
-
: undefined;
|
|
793
|
-
|
|
794
|
-
const result = await inviteMember({
|
|
795
|
-
target,
|
|
796
|
-
role: opts.role,
|
|
797
|
-
paths: opts.paths,
|
|
798
|
-
groupIds,
|
|
799
|
-
// Agents join actively — no Resend email.
|
|
800
|
-
sendEmail: detectTarget(target)?.isAgent ? false : opts.sendEmail,
|
|
801
|
-
companyUid,
|
|
802
|
-
callerUid,
|
|
803
|
-
token,
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
const agentTarget = detectTarget(target)?.isAgent === true;
|
|
807
|
-
console.log(
|
|
808
|
-
chalk.green(
|
|
809
|
-
`Invited ${target} as ${result.membership.role} (status: ${result.membership.status})`,
|
|
810
|
-
),
|
|
811
|
-
);
|
|
812
|
-
// Agent path is confirmed only when the server returned the guest-invite
|
|
813
|
-
// envelope (activationBilled present) AND status is active. A pending
|
|
814
|
-
// row means this hq-pro stage has not rolled the agent invite path yet —
|
|
815
|
-
// do NOT claim active join / no-double-bill.
|
|
816
|
-
if (
|
|
817
|
-
agentTarget &&
|
|
818
|
-
result.membership.status === "active" &&
|
|
819
|
-
typeof result.activationBilled === "boolean"
|
|
820
|
-
) {
|
|
821
|
-
console.log(
|
|
822
|
-
chalk.dim(
|
|
823
|
-
`Fleet agent: active membership granted immediately` +
|
|
824
|
-
(result.hostCompanyUid
|
|
825
|
-
? ` (host ${result.hostCompanyUid} keeps billing)`
|
|
826
|
-
: " (host keeps billing, no double charge)") +
|
|
827
|
-
`. Next: share secrets / vault paths as needed (\`hq secrets share\`, \`/new-agent\`).`,
|
|
828
|
-
),
|
|
829
|
-
);
|
|
830
|
-
console.log();
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
if (agentTarget && result.membership.status !== "active") {
|
|
834
|
-
console.log(
|
|
835
|
-
chalk.yellow(
|
|
836
|
-
"⚠ Agent invite landed as a pending membership — this hq-pro stage may not yet have multi-company agent invite. Wait for prod deploy of the agent membership path, then re-invite (or revoke this pending row).",
|
|
837
|
-
),
|
|
838
|
-
);
|
|
839
|
-
console.log();
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
console.log();
|
|
843
|
-
if (result.magicLink) {
|
|
844
|
-
// Legacy server schema — magic-link redemption.
|
|
845
|
-
console.log(chalk.bold("Magic link:"));
|
|
846
|
-
console.log(` ${result.magicLink}`);
|
|
847
|
-
console.log();
|
|
848
|
-
console.log(
|
|
849
|
-
chalk.dim(
|
|
850
|
-
"Share this link with the invitee. They can run `hq onboard join --invite-token <token>` to accept.",
|
|
851
|
-
),
|
|
852
|
-
);
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// schemaVersion 2+ — email-keyed authoritative membership row.
|
|
857
|
-
// Three output branches depending on whether the server-side
|
|
858
|
-
// Resend send fired:
|
|
859
|
-
// (a) emailSent === true → "email sent" success line
|
|
860
|
-
// (b) emailSkipped === true → server has no RESEND_API_KEY;
|
|
861
|
-
// fall back to the manual-notify instructions
|
|
862
|
-
// (c) emailSent/emailSkipped both undefined → pre-resend
|
|
863
|
-
// hq-pro, doesn't understand `sendEmail` at all; fall
|
|
864
|
-
// back to the manual-notify instructions
|
|
865
|
-
const inviteeEmail =
|
|
866
|
-
result.membership.inviteeEmail ??
|
|
867
|
-
(typeof target === "string" && target.includes("@")
|
|
868
|
-
? target
|
|
869
|
-
: undefined);
|
|
870
|
-
const emailDelivered = result.emailSent === true;
|
|
871
|
-
if (emailDelivered) {
|
|
872
|
-
printEmailStatus({
|
|
873
|
-
recipient: inviteeEmail ?? target,
|
|
874
|
-
sent: true,
|
|
875
|
-
skipped: false,
|
|
876
|
-
error: undefined,
|
|
877
|
-
});
|
|
878
|
-
if (result.membership.membershipKey) {
|
|
879
|
-
console.log();
|
|
880
|
-
console.log(
|
|
881
|
-
chalk.dim(`Membership key: ${result.membership.membershipKey}`),
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
// Email-send failed or wasn't attempted — surface the manual path.
|
|
887
|
-
if (result.emailError) {
|
|
888
|
-
console.log(
|
|
889
|
-
chalk.yellow(`⚠ Email send failed: ${result.emailError}`),
|
|
890
|
-
);
|
|
891
|
-
console.log();
|
|
892
|
-
}
|
|
893
|
-
console.log(
|
|
894
|
-
chalk.yellow(
|
|
895
|
-
"⚠ No email was sent. The pending membership row exists; the invitee needs to sign in manually to claim it.",
|
|
896
|
-
),
|
|
897
|
-
);
|
|
898
|
-
console.log();
|
|
899
|
-
console.log(chalk.bold("To complete the invite, do ONE of:"));
|
|
900
|
-
console.log(
|
|
901
|
-
` 1. Manually notify ${inviteeEmail ?? "the invitee"}: ask them to sign into HQ`,
|
|
902
|
-
);
|
|
903
|
-
console.log(
|
|
904
|
-
` at https://hq.computer with that email address.`,
|
|
905
|
-
);
|
|
906
|
-
console.log(
|
|
907
|
-
` 2. Or use the hq-console UI at https://hq.computer to issue the`,
|
|
908
|
-
);
|
|
909
|
-
console.log(
|
|
910
|
-
` invite instead — the UI path triggers an automated email via Resend.`,
|
|
911
|
-
);
|
|
912
|
-
if (result.emailSkipped !== true && result.emailSent === undefined) {
|
|
913
|
-
console.log();
|
|
914
|
-
console.log(
|
|
915
|
-
chalk.dim(
|
|
916
|
-
"Tip: this hq-pro deploy doesn't support server-side email yet (`sendEmail` ignored). Ask an operator to ship hq-pro PR #140 + provision the ResendApiKey SST secret.",
|
|
917
|
-
),
|
|
918
|
-
);
|
|
919
|
-
} else if (result.emailSkipped === true) {
|
|
920
|
-
console.log();
|
|
921
|
-
console.log(
|
|
922
|
-
chalk.dim(
|
|
923
|
-
"Tip: hq-pro reported `emailSkipped: true` — its `RESEND_API_KEY` SST secret isn't set for this stage.",
|
|
924
|
-
),
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
console.log();
|
|
928
|
-
console.log(
|
|
929
|
-
chalk.dim(
|
|
930
|
-
"The pending membership row claims itself on the invitee's first sign-in.",
|
|
931
|
-
),
|
|
932
|
-
);
|
|
933
|
-
if (result.membership.membershipKey) {
|
|
934
|
-
console.log();
|
|
935
|
-
console.log(
|
|
936
|
-
chalk.dim(`Membership key: ${result.membership.membershipKey}`),
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
} catch (err) {
|
|
940
|
-
// A 409 may refer to either an active membership or an existing
|
|
941
|
-
// pending invite. Probe both views so email targets get actionable
|
|
942
|
-
// guidance while preserving the terminal exit-0 behavior for bulk
|
|
943
|
-
// invite scripts.
|
|
944
|
-
if (
|
|
945
|
-
err instanceof InviteHttpError &&
|
|
946
|
-
err.code === "MEMBERSHIP_ALREADY_EXISTS"
|
|
947
|
-
) {
|
|
948
|
-
const detected = detectTarget(target);
|
|
949
|
-
const normalizedEmail =
|
|
950
|
-
detected?.type === "email" ? detected.value : undefined;
|
|
951
|
-
|
|
952
|
-
if (normalizedEmail && token && companyUid) {
|
|
953
|
-
// Preserve --company in copy-paste hints so multi-company
|
|
954
|
-
// callers don't hit "Multiple active companies" on --resend.
|
|
955
|
-
const companyFlag = companySlug
|
|
956
|
-
? ` --company ${companySlug}`
|
|
957
|
-
: "";
|
|
958
|
-
const resendHint =
|
|
959
|
-
`hq members invite ${normalizedEmail}${companyFlag} --resend`;
|
|
960
|
-
|
|
961
|
-
try {
|
|
962
|
-
const pending = await listPendingInvites(token, companyUid);
|
|
963
|
-
if (
|
|
964
|
-
pending.some(
|
|
965
|
-
(invite) =>
|
|
966
|
-
invite.inviteeEmail?.trim().toLowerCase() ===
|
|
967
|
-
normalizedEmail,
|
|
968
|
-
)
|
|
969
|
-
) {
|
|
970
|
-
console.log(
|
|
971
|
-
chalk.yellow(
|
|
972
|
-
`${normalizedEmail} already has a pending invite. ` +
|
|
973
|
-
`Use \`${resendHint}\` to re-send it.`,
|
|
974
|
-
),
|
|
975
|
-
);
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
} catch {
|
|
979
|
-
// Pending-list visibility can vary by server version or role.
|
|
980
|
-
// Fall through to the active-member probe and generic copy.
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
try {
|
|
984
|
-
const activeMembers = await listActiveMembers(token, companyUid);
|
|
985
|
-
if (
|
|
986
|
-
activeMembers.some(
|
|
987
|
-
(member) =>
|
|
988
|
-
member.personEmail?.trim().toLowerCase() ===
|
|
989
|
-
normalizedEmail,
|
|
990
|
-
)
|
|
991
|
-
) {
|
|
992
|
-
console.log(
|
|
993
|
-
chalk.yellow(
|
|
994
|
-
`${normalizedEmail} is already a member of this company — nothing to do.`,
|
|
995
|
-
),
|
|
996
|
-
);
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
} catch {
|
|
1000
|
-
// Preserve the original conflict as the source of truth and
|
|
1001
|
-
// use copy that remains accurate when the probes are hidden.
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
console.log(
|
|
1005
|
-
chalk.yellow(
|
|
1006
|
-
`${normalizedEmail} already has a membership or pending invite for this company. ` +
|
|
1007
|
-
`If the invite is pending, use \`${resendHint}\`.`,
|
|
1008
|
-
),
|
|
1009
|
-
);
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
console.log(
|
|
1014
|
-
chalk.yellow(
|
|
1015
|
-
`${target.toLowerCase()} is already a member of this company — nothing to do.`,
|
|
1016
|
-
),
|
|
1017
|
-
);
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
if (err instanceof InviteHttpError) {
|
|
1021
|
-
console.error(
|
|
1022
|
-
chalk.red(
|
|
1023
|
-
formatInviteHttpError(err.status, err.message, err.code),
|
|
1024
|
-
),
|
|
1025
|
-
);
|
|
1026
|
-
process.exit(1);
|
|
1027
|
-
}
|
|
1028
|
-
console.error(
|
|
1029
|
-
chalk.red("Error:"),
|
|
1030
|
-
err instanceof Error ? err.message : String(err),
|
|
1031
|
-
);
|
|
1032
|
-
process.exit(1);
|
|
1033
|
-
}
|
|
1034
|
-
},
|
|
1035
|
-
);
|
|
1036
|
-
|
|
1037
|
-
members
|
|
1038
|
-
.command("list")
|
|
1039
|
-
.description(
|
|
1040
|
-
"List the company's active members (use --pending for pending invites)",
|
|
1041
|
-
)
|
|
1042
|
-
.option("--pending", "List pending invites instead of active members")
|
|
1043
|
-
.action(async (opts: { pending?: boolean }) => {
|
|
1044
|
-
try {
|
|
1045
|
-
const token = await ensureCognitoToken();
|
|
1046
|
-
const companySlug = members.opts().company as string | undefined;
|
|
1047
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
1048
|
-
|
|
1049
|
-
if (opts.pending) {
|
|
1050
|
-
// --pending: preserve the original pending-invites view verbatim.
|
|
1051
|
-
const invites = await listPendingInvites(token, companyUid);
|
|
1052
|
-
|
|
1053
|
-
if (invites.length === 0) {
|
|
1054
|
-
console.log(chalk.gray("No pending invites for this company."));
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
const targetW = Math.max(
|
|
1059
|
-
6,
|
|
1060
|
-
...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length),
|
|
1061
|
-
);
|
|
1062
|
-
const roleW = Math.max(4, ...invites.map((i) => i.role.length));
|
|
1063
|
-
const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
|
|
1064
|
-
const keyW = Math.max(
|
|
1065
|
-
14,
|
|
1066
|
-
...invites.map((i) => i.membershipKey.length),
|
|
1067
|
-
);
|
|
1068
|
-
console.log(
|
|
1069
|
-
chalk.bold(
|
|
1070
|
-
[
|
|
1071
|
-
"TARGET".padEnd(targetW),
|
|
1072
|
-
"ROLE".padEnd(roleW),
|
|
1073
|
-
"INVITED_BY".padEnd(byW),
|
|
1074
|
-
"INVITED_AT",
|
|
1075
|
-
"MEMBERSHIP_KEY".padEnd(keyW),
|
|
1076
|
-
].join(" "),
|
|
1077
|
-
),
|
|
1078
|
-
);
|
|
1079
|
-
for (const inv of invites) {
|
|
1080
|
-
const target = inv.inviteeEmail ?? inv.personUid ?? "";
|
|
1081
|
-
console.log(
|
|
1082
|
-
[
|
|
1083
|
-
target.padEnd(targetW),
|
|
1084
|
-
inv.role.padEnd(roleW),
|
|
1085
|
-
inv.invitedBy.padEnd(byW),
|
|
1086
|
-
shortDate(inv.invitedAt),
|
|
1087
|
-
inv.membershipKey.padEnd(keyW),
|
|
1088
|
-
].join(" "),
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// Default: list ACTIVE members so their emails drop straight into
|
|
1095
|
-
// `hq secrets share <path> --with <email>`.
|
|
1096
|
-
const activeMembers = await listActiveMembers(token, companyUid);
|
|
1097
|
-
|
|
1098
|
-
if (activeMembers.length === 0) {
|
|
1099
|
-
console.log(chalk.gray("No active members found for this company."));
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const emailW = Math.max(
|
|
1104
|
-
5,
|
|
1105
|
-
...activeMembers.map((m) => (m.personEmail ?? m.personUid).length),
|
|
1106
|
-
);
|
|
1107
|
-
const roleW = Math.max(4, ...activeMembers.map((m) => m.role.length));
|
|
1108
|
-
console.log(
|
|
1109
|
-
chalk.bold(
|
|
1110
|
-
["EMAIL".padEnd(emailW), "ROLE".padEnd(roleW), "NAME"].join(" "),
|
|
1111
|
-
),
|
|
1112
|
-
);
|
|
1113
|
-
for (const m of activeMembers) {
|
|
1114
|
-
const email = m.personEmail ?? m.personUid;
|
|
1115
|
-
const name = m.personName ?? m.personSlug ?? "";
|
|
1116
|
-
console.log(
|
|
1117
|
-
[email.padEnd(emailW), m.role.padEnd(roleW), name].join(" "),
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
console.log(
|
|
1121
|
-
chalk.gray(
|
|
1122
|
-
"Share a secret with a member: hq secrets share <path> --with <email>",
|
|
1123
|
-
),
|
|
1124
|
-
);
|
|
1125
|
-
} catch (err) {
|
|
1126
|
-
if (err instanceof InviteHttpError) {
|
|
1127
|
-
const msg =
|
|
1128
|
-
err.status === 403
|
|
1129
|
-
? opts.pending
|
|
1130
|
-
? "Not authorized — only admins and owners can list invites"
|
|
1131
|
-
: "Not authorized — only company members can list members"
|
|
1132
|
-
: formatInviteHttpError(err.status, err.message);
|
|
1133
|
-
console.error(chalk.red(msg));
|
|
1134
|
-
process.exit(1);
|
|
1135
|
-
}
|
|
1136
|
-
console.error(
|
|
1137
|
-
chalk.red("Error:"),
|
|
1138
|
-
err instanceof Error ? err.message : String(err),
|
|
1139
|
-
);
|
|
1140
|
-
process.exit(1);
|
|
1141
|
-
}
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
members
|
|
1145
|
-
.command("revoke <target>")
|
|
1146
|
-
.description(
|
|
1147
|
-
"Revoke a pending invite. Accepts an email, personUid, full membershipKey, or legacy inviteToken.",
|
|
1148
|
-
)
|
|
1149
|
-
.action(async (target: string) => {
|
|
1150
|
-
try {
|
|
1151
|
-
const token = await ensureCognitoToken();
|
|
1152
|
-
const companySlug = members.opts().company as string | undefined;
|
|
1153
|
-
const companyUid = await getCompanyUid(token, companySlug);
|
|
1154
|
-
|
|
1155
|
-
const membershipKey = resolveRevokeTargetToMembershipKey(
|
|
1156
|
-
target,
|
|
1157
|
-
companyUid,
|
|
1158
|
-
);
|
|
1159
|
-
await revokeInvite(token, membershipKey, companyUid);
|
|
1160
|
-
console.log(chalk.green(`Revoked invite '${membershipKey}'`));
|
|
1161
|
-
} catch (err) {
|
|
1162
|
-
if (err instanceof InviteHttpError) {
|
|
1163
|
-
const msg =
|
|
1164
|
-
err.status === 403
|
|
1165
|
-
? err.code === "ADMIN_ROLE_TARGET_RESTRICTED"
|
|
1166
|
-
? "Admin can only revoke role=member; ask an owner to revoke this membership"
|
|
1167
|
-
: "Not authorized — only admins and owners can revoke invites"
|
|
1168
|
-
: err.status === 404
|
|
1169
|
-
? "Invite not found — it may have already been accepted or revoked"
|
|
1170
|
-
: formatInviteHttpError(err.status, err.message, err.code);
|
|
1171
|
-
console.error(chalk.red(msg));
|
|
1172
|
-
process.exit(1);
|
|
1173
|
-
}
|
|
1174
|
-
console.error(
|
|
1175
|
-
chalk.red("Error:"),
|
|
1176
|
-
err instanceof Error ? err.message : String(err),
|
|
1177
|
-
);
|
|
1178
|
-
process.exit(1);
|
|
1179
|
-
}
|
|
1180
|
-
});
|
|
1181
|
-
}
|