@indigoai-us/hq-cli 5.345.163 → 5.345.165
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 +36 -0
- package/dist/command-catalog.generated.d.ts +66 -0
- package/dist/command-catalog.generated.js +82 -0
- package/dist/commands/billing-sponsor.d.ts +20 -0
- package/dist/commands/billing-sponsor.js +116 -0
- package/dist/commands/billing-usage.d.ts +16 -2
- package/dist/commands/billing-usage.js +63 -9
- package/dist/commands/billing.d.ts +34 -0
- package/dist/commands/billing.js +108 -1
- package/dist/commands/deploy-domains.d.ts +2 -0
- package/dist/commands/deploy-domains.js +18 -3
- package/dist/commands/scan.d.ts +6 -0
- package/dist/commands/scan.js +11 -1
- package/dist/lib/billing/billing-status-lines.d.ts +14 -0
- package/dist/lib/billing/billing-status-lines.js +43 -0
- package/dist/lib/billing/usage-only.d.ts +18 -0
- package/dist/lib/billing/usage-only.js +18 -1
- package/dist/lib/doctor/checks/sync-runtime.d.ts +34 -0
- package/dist/lib/doctor/checks/sync-runtime.js +217 -8
- package/dist/lib/doctor/fix/apply.js +1 -0
- package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +4 -1
- package/dist/lib/doctor/fix/refresh-sync-outcome.js +15 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +42 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.js +100 -0
- package/dist/lib/doctor/json-output.d.ts +7 -0
- package/dist/lib/doctor/json-output.js +8 -1
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.js +121 -38
- package/dist/lib/mesh/live/paths.d.ts +1 -0
- package/dist/lib/mesh/live/paths.js +1 -0
- package/dist/lib/secret-scan/detect.js +2 -0
- package/dist/lib/secret-scan/probes.d.ts +13 -0
- package/dist/lib/secret-scan/probes.js +69 -0
- package/dist/lib/secret-scan/repo-config.d.ts +24 -0
- package/dist/lib/secret-scan/repo-config.js +159 -29
- package/dist/lib/secret-scan/rules.d.ts +2 -0
- package/dist/lib/secret-scan/staged.d.ts +15 -1
- package/dist/lib/secret-scan/staged.js +46 -4
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.165] — 2026-10-23
|
|
6
|
+
- `hq billing sponsor grant <email|prs_id> --company <slug>` lets a company owner or admin put a member's personal plan on the company bill without a request first; `--all-members` does it for every active member and reports each person as covered, already covered, or skipped. `hq billing sponsor ungrant <email|prs_id>` takes it off again. hq-pro checks the owner/admin role and active membership.
|
|
7
|
+
- `hq scan secrets <files...>` takes `--repo <dir>`, which applies that repo's committed `.gitleaks.toml` allowlists to a file scan, matching each file by its path relative to the repo, as a staged scan does. `--snapshot-root <dir>` (with `--repo`) matches files laid out as `<dir>/<commit>/<repo path>`, the history copies `commit-secret-scan.sh pr-check` writes, as `<repo path>`. Findings still name the files as given. Before this, a file scan ignored the repo's allowlists, so pr-check failed on false positives the commit gate already allowed. Without `--repo` a file scan is unchanged.
|
|
8
|
+
- `--allowlist-rev <rev>` (with `--repo`) reads `.gitleaks.toml` from that commit instead of HEAD; pr-check passes its base branch, so a branch cannot exempt its own earlier commits by adding an allowlist later. An unknown rev exits 2.
|
|
9
|
+
- A file that resolves (after `..` and symlinks) outside the repo and the snapshot root gets no repo allowlist, nor does a snapshot file without a commit folder.
|
|
10
|
+
- A file scan with `--repo` honors only exact-value exemptions. Each allowlist needs `condition = "AND"` and specific `paths`, and lists in `stopwords` the exact secret values it allows, such as known manifest digests (at least 16 characters, no spaces). A detected secret is allowed only when it equals one of them, so other secrets in the same file are still reported. An allowlist with `regexes`, `regexTarget`, `targetRules` or `commits`, or with paths alone, is refused and the scan exits 2, because patterns, alone or combined across entries, can hide a real credential. Staged scans keep the full allowlist format; there each allowlist with content checks is judged by running the real detector on a fixed set of realistic credentials and refused if it hides any of them.
|
|
11
|
+
- An allowlist validation error never prints an exemption value. It names the entry and position (for example `stopwords[0]`) with the value's length and a short SHA-256 prefix, so a credential typed into an allowlist by mistake does not reach terminal or CI logs.
|
|
12
|
+
|
|
13
|
+
## [5.345.164] — 2026-10-23
|
|
14
|
+
- `hq billing status` and `hq billing usage --card` now show trial usage against the $500 trial cap, the $10 included usage, the spend limit and a prompt to add a card when usage passes the included amount. The plan line says "unavailable right now" instead of guessing Starter, invoice-billed companies read as a flat plan billed by invoice, and a failed read of a scheduled switch is reported.
|
|
15
|
+
- `hq deploy domains` refuses a name that is really an IPv4 address. Inputs such as `1.2`, `0x7f.1` and `1.2.3.4` were rewritten to an address and accepted; a numeric last label is now refused before any request.
|
|
16
|
+
- `hq deploy domains buy`: when the domain is registered but the card charge does not complete, the output now prints the order's idempotency key and says to run the same command again with it to retry the charge. It no longer says that HQ billing follows up, which nothing did.
|
|
17
|
+
- `--company` that matches the folder's link in a different case is accepted, and the link's slug is sent.
|
|
18
|
+
- `hq doctor` now says which cloud call the sync receiver was refused on. `sync.runtime.receiver-permission` breaks its count down by call class (the per-device queue credentials from `POST /v1/sync/subscribe`, the subscribe call itself, the latency metric, or the object pull after a change event) and gives the matching next step, instead of one bare count. A new `sync.runtime.pull-access-denied` check warns when the same vault key was refused on two or more pulls in the hour and names the repeatedly refused keys (relative keys only, at most 10, with the runner's latest reason tag; a key refused once is counted but not named). In a session locked to companies, only keys from those companies are named and the rest are counted, so doctor never prints another company's file names from the machine-wide sync log; a refusal with no company label is shown as `unknown` with no lookup command. The warning gives the `hq files acl` command to compare the stored grant with the key the pull looked up. Before this, every automated client-health report for a refused receiver carried only a count and could not be acted on (feedback_cd56a501 and 27 linked reports).
|
|
19
|
+
- A stale sync journal whose `hq doctor --fix` pull ended offline, timed out, or hit a server 5xx is no longer reported as unrepaired by the client-health hook: doctor JSON leaves those rows out for 15 minutes after the attempt and lists them under `transientRefreshSyncCheckIds`, so the next SessionStart retries the pull instead of filing a report. A pull the service refused (4xx) or that failed for another reason is still reported. The `--fix` progress file records each journal's pull outcome as additive fields (still version 2, so an older CLI sharing the same HQ root keeps reading it, and an outcome value this CLI does not know is dropped rather than failing `--fix`); a journal that has failed transiently for more than 24 hours is reported again.
|
|
20
|
+
- `hq-hook bridge-plan` now also lists bridged helpers: `derive-trigger-facts`
|
|
21
|
+
appears when its flag `hook-engine-port-derive-trigger-facts` is `shadow` or
|
|
22
|
+
`on`. master-hook.sh passes the plan to the policy injector, which then runs
|
|
23
|
+
its fact helper through `hq-hook bridge derive-trigger-facts` instead of
|
|
24
|
+
bash (US-008). The flag defaults to off, the kill switch still forces bash,
|
|
25
|
+
and `hq-hook --self-check` reports the list as `helper_bridge_ports`. The
|
|
26
|
+
engine version is now 0.3.0.
|
|
27
|
+
- `hq-hook run policy-enforcement-gate` is the first hook port master-hook.sh
|
|
28
|
+
can bridge (US-022, flag `hook-engine-port-policy-enforcement-gate`, default
|
|
29
|
+
off). It answers the gate's allow paths without starting a process: no
|
|
30
|
+
session id, no session state, a PostToolUse with no state, an event the gate
|
|
31
|
+
does not handle, and a PreToolUse that is not outbound, matches no gate
|
|
32
|
+
policy and has no requirement to merge. For a Bash PreToolUse it writes the
|
|
33
|
+
same pre-tool pass stamp the bash gate writes. Every other case (outbound
|
|
34
|
+
tools and commands, gate policies, requirement merges, receipts,
|
|
35
|
+
UserPromptSubmit, non-ASCII input, jq not found on PATH, anything it cannot
|
|
36
|
+
parse) exits 75, and
|
|
37
|
+
the bridge runs the bash gate. The port never blocks.
|
|
38
|
+
- HQ Sync now reacts to secure real-time wake notifications on macOS, so shared
|
|
39
|
+
vault changes can start syncing within seconds without frequent polling.
|
|
40
|
+
|
|
5
41
|
## [5.345.163] — 2026-10-23
|
|
6
42
|
- New `hq deploy domains` manages custom domains on a deployed app from the installed CLI, using the same hq-deploy routes as the desktop Deployments panel. `list`, `add <domain>`, `refresh <domain>` and `remove <domain>` take `--app <name or id>`, or use the app linked to the folder (`.hq/project.json`, or the older `hq-deploy.json`; `--path` picks the folder). Domains are checked before any request: hostname labels only (no URL, path, port or `@`), and an internationalized name is sent as punycode. A folder link that names no company needs `--company`, and a `--company` that differs from the link is refused. `add` names the app and company, shows the add-on price from `GET /v1/pricing`, and asks for approval (or `--yes`) before anything is added, then prints the DNS records to create, the guidance for a bare domain, and the billing link when the company has no card. `refresh` exits 2 while the domain is not live, so scripts can poll. `remove` names the app and company, asks for approval (or `--yes`), and says when the billing removal is queued for retry. Member, entitlement and billing errors print plain messages, and every command takes `--json`.
|
|
7
43
|
- New `hq deploy domains search <query>` and `hq deploy domains buy <domain> --price <usd> --registrant-file <path>` buy a domain for the company through hq-pro's domain purchase routes. `buy` names the company that pays and shows the first-year price and the published renewal and refund terms, and asks for approval (or `--yes`). It sends one idempotency key per order and only the documented registrant fields. Whenever the outcome is not known (a network error, a server error, a pending registration), it prints the key and says to re-run with `--idempotency-key` instead of starting a new order. Buying stays unavailable until the server has registrar credentials; until then the command says nothing was charged.
|
|
@@ -6277,6 +6277,18 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
6277
6277
|
readonly flags: "--json";
|
|
6278
6278
|
readonly description: "Print findings as JSON";
|
|
6279
6279
|
readonly hidden: false;
|
|
6280
|
+
}, {
|
|
6281
|
+
readonly flags: "--repo <dir>";
|
|
6282
|
+
readonly description: "With files: apply this repo's committed .gitleaks.toml allowlists, matching repo-relative paths";
|
|
6283
|
+
readonly hidden: false;
|
|
6284
|
+
}, {
|
|
6285
|
+
readonly flags: "--snapshot-root <dir>";
|
|
6286
|
+
readonly description: "With --repo: files under <dir>/<commit>/<path> are matched as <path>";
|
|
6287
|
+
readonly hidden: false;
|
|
6288
|
+
}, {
|
|
6289
|
+
readonly flags: "--allowlist-rev <rev>";
|
|
6290
|
+
readonly description: "With --repo: read .gitleaks.toml from this commit instead of HEAD";
|
|
6291
|
+
readonly hidden: false;
|
|
6280
6292
|
}];
|
|
6281
6293
|
readonly subcommands: readonly [];
|
|
6282
6294
|
}];
|
|
@@ -8820,6 +8832,60 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8820
8832
|
readonly hidden: false;
|
|
8821
8833
|
}];
|
|
8822
8834
|
readonly subcommands: readonly [];
|
|
8835
|
+
}, {
|
|
8836
|
+
readonly name: "grant";
|
|
8837
|
+
readonly description: "Admin: put a member's personal plan on the company bill, no request needed";
|
|
8838
|
+
readonly aliases: readonly [];
|
|
8839
|
+
readonly hidden: false;
|
|
8840
|
+
readonly helpOption: true;
|
|
8841
|
+
readonly helpCommand: false;
|
|
8842
|
+
readonly usage: "[options] [person]";
|
|
8843
|
+
readonly helpText: readonly [];
|
|
8844
|
+
readonly arguments: readonly [{
|
|
8845
|
+
readonly name: "person";
|
|
8846
|
+
readonly required: false;
|
|
8847
|
+
readonly variadic: false;
|
|
8848
|
+
readonly description: "Member email or prs_ id";
|
|
8849
|
+
}];
|
|
8850
|
+
readonly options: readonly [{
|
|
8851
|
+
readonly flags: "--company <slug>";
|
|
8852
|
+
readonly description: "Company slug (resolves to companyUid)";
|
|
8853
|
+
readonly hidden: false;
|
|
8854
|
+
}, {
|
|
8855
|
+
readonly flags: "--all-members";
|
|
8856
|
+
readonly description: "Sponsor every active member of the company";
|
|
8857
|
+
readonly hidden: false;
|
|
8858
|
+
}, {
|
|
8859
|
+
readonly flags: "--json";
|
|
8860
|
+
readonly description: "Emit raw JSON";
|
|
8861
|
+
readonly hidden: false;
|
|
8862
|
+
}];
|
|
8863
|
+
readonly subcommands: readonly [];
|
|
8864
|
+
}, {
|
|
8865
|
+
readonly name: "ungrant";
|
|
8866
|
+
readonly description: "Admin: take a member's personal plan off the company bill";
|
|
8867
|
+
readonly aliases: readonly [];
|
|
8868
|
+
readonly hidden: false;
|
|
8869
|
+
readonly helpOption: true;
|
|
8870
|
+
readonly helpCommand: false;
|
|
8871
|
+
readonly usage: "[options] <person>";
|
|
8872
|
+
readonly helpText: readonly [];
|
|
8873
|
+
readonly arguments: readonly [{
|
|
8874
|
+
readonly name: "person";
|
|
8875
|
+
readonly required: true;
|
|
8876
|
+
readonly variadic: false;
|
|
8877
|
+
readonly description: "Member email or prs_ id";
|
|
8878
|
+
}];
|
|
8879
|
+
readonly options: readonly [{
|
|
8880
|
+
readonly flags: "--company <slug>";
|
|
8881
|
+
readonly description: "Company slug (resolves to companyUid)";
|
|
8882
|
+
readonly hidden: false;
|
|
8883
|
+
}, {
|
|
8884
|
+
readonly flags: "--json";
|
|
8885
|
+
readonly description: "Emit raw JSON";
|
|
8886
|
+
readonly hidden: false;
|
|
8887
|
+
}];
|
|
8888
|
+
readonly subcommands: readonly [];
|
|
8823
8889
|
}];
|
|
8824
8890
|
}, {
|
|
8825
8891
|
readonly name: "usage";
|
|
@@ -7634,6 +7634,21 @@ export const COMMAND_CATALOG = [
|
|
|
7634
7634
|
"flags": "--json",
|
|
7635
7635
|
"description": "Print findings as JSON",
|
|
7636
7636
|
"hidden": false
|
|
7637
|
+
},
|
|
7638
|
+
{
|
|
7639
|
+
"flags": "--repo <dir>",
|
|
7640
|
+
"description": "With files: apply this repo's committed .gitleaks.toml allowlists, matching repo-relative paths",
|
|
7641
|
+
"hidden": false
|
|
7642
|
+
},
|
|
7643
|
+
{
|
|
7644
|
+
"flags": "--snapshot-root <dir>",
|
|
7645
|
+
"description": "With --repo: files under <dir>/<commit>/<path> are matched as <path>",
|
|
7646
|
+
"hidden": false
|
|
7647
|
+
},
|
|
7648
|
+
{
|
|
7649
|
+
"flags": "--allowlist-rev <rev>",
|
|
7650
|
+
"description": "With --repo: read .gitleaks.toml from this commit instead of HEAD",
|
|
7651
|
+
"hidden": false
|
|
7637
7652
|
}
|
|
7638
7653
|
],
|
|
7639
7654
|
"subcommands": []
|
|
@@ -10757,6 +10772,73 @@ export const COMMAND_CATALOG = [
|
|
|
10757
10772
|
}
|
|
10758
10773
|
],
|
|
10759
10774
|
"subcommands": []
|
|
10775
|
+
},
|
|
10776
|
+
{
|
|
10777
|
+
"name": "grant",
|
|
10778
|
+
"description": "Admin: put a member's personal plan on the company bill, no request needed",
|
|
10779
|
+
"aliases": [],
|
|
10780
|
+
"hidden": false,
|
|
10781
|
+
"helpOption": true,
|
|
10782
|
+
"helpCommand": false,
|
|
10783
|
+
"usage": "[options] [person]",
|
|
10784
|
+
"helpText": [],
|
|
10785
|
+
"arguments": [
|
|
10786
|
+
{
|
|
10787
|
+
"name": "person",
|
|
10788
|
+
"required": false,
|
|
10789
|
+
"variadic": false,
|
|
10790
|
+
"description": "Member email or prs_ id"
|
|
10791
|
+
}
|
|
10792
|
+
],
|
|
10793
|
+
"options": [
|
|
10794
|
+
{
|
|
10795
|
+
"flags": "--company <slug>",
|
|
10796
|
+
"description": "Company slug (resolves to companyUid)",
|
|
10797
|
+
"hidden": false
|
|
10798
|
+
},
|
|
10799
|
+
{
|
|
10800
|
+
"flags": "--all-members",
|
|
10801
|
+
"description": "Sponsor every active member of the company",
|
|
10802
|
+
"hidden": false
|
|
10803
|
+
},
|
|
10804
|
+
{
|
|
10805
|
+
"flags": "--json",
|
|
10806
|
+
"description": "Emit raw JSON",
|
|
10807
|
+
"hidden": false
|
|
10808
|
+
}
|
|
10809
|
+
],
|
|
10810
|
+
"subcommands": []
|
|
10811
|
+
},
|
|
10812
|
+
{
|
|
10813
|
+
"name": "ungrant",
|
|
10814
|
+
"description": "Admin: take a member's personal plan off the company bill",
|
|
10815
|
+
"aliases": [],
|
|
10816
|
+
"hidden": false,
|
|
10817
|
+
"helpOption": true,
|
|
10818
|
+
"helpCommand": false,
|
|
10819
|
+
"usage": "[options] <person>",
|
|
10820
|
+
"helpText": [],
|
|
10821
|
+
"arguments": [
|
|
10822
|
+
{
|
|
10823
|
+
"name": "person",
|
|
10824
|
+
"required": true,
|
|
10825
|
+
"variadic": false,
|
|
10826
|
+
"description": "Member email or prs_ id"
|
|
10827
|
+
}
|
|
10828
|
+
],
|
|
10829
|
+
"options": [
|
|
10830
|
+
{
|
|
10831
|
+
"flags": "--company <slug>",
|
|
10832
|
+
"description": "Company slug (resolves to companyUid)",
|
|
10833
|
+
"hidden": false
|
|
10834
|
+
},
|
|
10835
|
+
{
|
|
10836
|
+
"flags": "--json",
|
|
10837
|
+
"description": "Emit raw JSON",
|
|
10838
|
+
"hidden": false
|
|
10839
|
+
}
|
|
10840
|
+
],
|
|
10841
|
+
"subcommands": []
|
|
10760
10842
|
}
|
|
10761
10843
|
]
|
|
10762
10844
|
},
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* hq billing sponsor decline <id> — admin: say no
|
|
14
14
|
* hq billing sponsor revoke <id> — admin: end an active sponsorship
|
|
15
15
|
* hq billing sponsor withdraw <id> — me: take back my own request
|
|
16
|
+
* hq billing sponsor grant <person> --company <slug>
|
|
17
|
+
* — admin: sponsor a member's plan
|
|
18
|
+
* directly, no request needed
|
|
19
|
+
* hq billing sponsor grant --all-members --company <slug>
|
|
20
|
+
* — admin: sponsor every active member
|
|
21
|
+
* hq billing sponsor ungrant <person> --company <slug>
|
|
22
|
+
* — admin: end a direct sponsorship
|
|
16
23
|
*
|
|
17
24
|
* Every subcommand talks to the SAME hq-pro routes the console uses
|
|
18
25
|
* (`/v1/billing/sponsorship/...`). hq-pro is the authoritative gate on all of
|
|
@@ -82,6 +89,19 @@ export interface SponsorshipSelfState {
|
|
|
82
89
|
}
|
|
83
90
|
/** Plain label for a request status. Never shows the raw enum. */
|
|
84
91
|
export declare function statusLabel(status: string): string;
|
|
92
|
+
/** Per-person outcome of an owner/admin grant, as hq-pro returns it. */
|
|
93
|
+
export interface PlanGrantResult {
|
|
94
|
+
personUid: string;
|
|
95
|
+
resourceId: string;
|
|
96
|
+
outcome: "sponsored" | "already_sponsored" | "sponsored_by_another_company" | "usage_pricing" | "not_a_member";
|
|
97
|
+
}
|
|
98
|
+
/** Plain line for one grant outcome. */
|
|
99
|
+
export declare function grantOutcomeLabel(outcome: PlanGrantResult["outcome"]): string;
|
|
100
|
+
/**
|
|
101
|
+
* Turn `<person>` into a person uid: a `prs_` uid passes through, an email is
|
|
102
|
+
* looked up in the company's active members. hq-pro re-checks membership.
|
|
103
|
+
*/
|
|
104
|
+
export declare function resolveMemberPersonUid(token: string, companyUid: string, person: string): Promise<string>;
|
|
85
105
|
export declare function registerBillingSponsorCommands(billing: Command): void;
|
|
86
106
|
export {};
|
|
87
107
|
//# sourceMappingURL=billing-sponsor.d.ts.map
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* hq billing sponsor decline <id> — admin: say no
|
|
14
14
|
* hq billing sponsor revoke <id> — admin: end an active sponsorship
|
|
15
15
|
* hq billing sponsor withdraw <id> — me: take back my own request
|
|
16
|
+
* hq billing sponsor grant <person> --company <slug>
|
|
17
|
+
* — admin: sponsor a member's plan
|
|
18
|
+
* directly, no request needed
|
|
19
|
+
* hq billing sponsor grant --all-members --company <slug>
|
|
20
|
+
* — admin: sponsor every active member
|
|
21
|
+
* hq billing sponsor ungrant <person> --company <slug>
|
|
22
|
+
* — admin: end a direct sponsorship
|
|
16
23
|
*
|
|
17
24
|
* Every subcommand talks to the SAME hq-pro routes the console uses
|
|
18
25
|
* (`/v1/billing/sponsorship/...`). hq-pro is the authoritative gate on all of
|
|
@@ -35,6 +42,7 @@ import { diagnosticsEnabled } from "../lib/diagnostics.js";
|
|
|
35
42
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
36
43
|
import { isCompanySelectionError } from "../utils/company-selection-error.js";
|
|
37
44
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
45
|
+
import { listActiveMembers } from "./members.js";
|
|
38
46
|
import { fetchCompanyPricingPlan, fetchPersonPricingPlan, isUsageOnly, usageOnlySponsorLines, } from "../lib/billing/usage-only.js";
|
|
39
47
|
/** The request kind this command files. Matches hq-pro's contract. */
|
|
40
48
|
export const PLAN_RESOURCE = "plan";
|
|
@@ -90,6 +98,9 @@ export function messageForError(status, body) {
|
|
|
90
98
|
if (status === 403) {
|
|
91
99
|
return "You're not allowed to do that. Only an owner or admin of the company can answer a sponsorship request.";
|
|
92
100
|
}
|
|
101
|
+
if (status === 404 && code === "NOT_A_MEMBER") {
|
|
102
|
+
return "That person isn't an active member of this company.";
|
|
103
|
+
}
|
|
93
104
|
if (status === 404)
|
|
94
105
|
return "No sponsorship request with that id.";
|
|
95
106
|
if (status === 409 && code === "DUPLICATE_PENDING_REQUEST") {
|
|
@@ -168,6 +179,41 @@ async function transition(requestId, op) {
|
|
|
168
179
|
});
|
|
169
180
|
return { ...data.request, code: data.code, addCardUrl: data.addCardUrl };
|
|
170
181
|
}
|
|
182
|
+
/** Plain line for one grant outcome. */
|
|
183
|
+
export function grantOutcomeLabel(outcome) {
|
|
184
|
+
switch (outcome) {
|
|
185
|
+
case "sponsored":
|
|
186
|
+
return "Now on the company bill";
|
|
187
|
+
case "already_sponsored":
|
|
188
|
+
return "Already on the company bill";
|
|
189
|
+
case "sponsored_by_another_company":
|
|
190
|
+
return "Skipped: another company already covers them";
|
|
191
|
+
case "usage_pricing":
|
|
192
|
+
return "Skipped: on usage pricing, no personal plan to cover";
|
|
193
|
+
case "not_a_member":
|
|
194
|
+
return "Skipped: not an active member";
|
|
195
|
+
default:
|
|
196
|
+
return outcome;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Turn `<person>` into a person uid: a `prs_` uid passes through, an email is
|
|
201
|
+
* looked up in the company's active members. hq-pro re-checks membership.
|
|
202
|
+
*/
|
|
203
|
+
export async function resolveMemberPersonUid(token, companyUid, person) {
|
|
204
|
+
const trimmed = person.trim();
|
|
205
|
+
if (/^prs_[A-Za-z0-9]+$/.test(trimmed))
|
|
206
|
+
return trimmed;
|
|
207
|
+
const email = trimmed.toLowerCase();
|
|
208
|
+
if (!/^[^\s]+@[^\s]+$/.test(email)) {
|
|
209
|
+
throw new SponsorError("Pass the person's email or their prs_ id.");
|
|
210
|
+
}
|
|
211
|
+
const members = await listActiveMembers(token, companyUid);
|
|
212
|
+
const match = members.find((m) => m.personEmail?.trim().toLowerCase() === email);
|
|
213
|
+
if (match?.personUid)
|
|
214
|
+
return match.personUid;
|
|
215
|
+
throw new SponsorError(`No active member has the email ${email}. Run \`hq members list\`.`);
|
|
216
|
+
}
|
|
171
217
|
/** Whether the sponsoring company is on usage pricing. Unknown counts as no. */
|
|
172
218
|
async function companyIsUsageOnly(companyUid) {
|
|
173
219
|
if (!companyUid)
|
|
@@ -402,5 +448,75 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
402
448
|
fail(err);
|
|
403
449
|
}
|
|
404
450
|
});
|
|
451
|
+
sponsor
|
|
452
|
+
.command("grant")
|
|
453
|
+
.argument("[person]", "Member email or prs_ id")
|
|
454
|
+
.description("Admin: put a member's personal plan on the company bill, no request needed")
|
|
455
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
456
|
+
.option("--all-members", "Sponsor every active member of the company")
|
|
457
|
+
.option("--json", "Emit raw JSON")
|
|
458
|
+
.action(async function (person, opts) {
|
|
459
|
+
try {
|
|
460
|
+
if (Boolean(person) === Boolean(opts.allMembers)) {
|
|
461
|
+
throw new SponsorError("Name one person, or pass --all-members.");
|
|
462
|
+
}
|
|
463
|
+
const token = await ensureCognitoToken();
|
|
464
|
+
const named = opts.company ?? billing.opts().company;
|
|
465
|
+
const companyUid = await getCompanyUid(token, named);
|
|
466
|
+
const body = { companyUid };
|
|
467
|
+
if (person)
|
|
468
|
+
body.personUid = await resolveMemberPersonUid(token, companyUid, person);
|
|
469
|
+
else
|
|
470
|
+
body.allMembers = true;
|
|
471
|
+
const data = await callApi(token, "/v1/billing/sponsorship/plans", { method: "POST", body });
|
|
472
|
+
if (opts.json) {
|
|
473
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (data.results.length === 0) {
|
|
477
|
+
console.log("No active members to sponsor.");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
for (const r of data.results) {
|
|
481
|
+
console.log(` ${chalk.bold(r.personUid)} ${grantOutcomeLabel(r.outcome)}`);
|
|
482
|
+
}
|
|
483
|
+
const added = data.results.filter((r) => r.outcome === "sponsored").length;
|
|
484
|
+
console.log(added === 1
|
|
485
|
+
? "1 personal plan moved onto the company bill."
|
|
486
|
+
: `${added} personal plans moved onto the company bill.`);
|
|
487
|
+
const single = person ? data.results[0] : undefined;
|
|
488
|
+
if (single && single.outcome !== "sponsored" && single.outcome !== "already_sponsored") {
|
|
489
|
+
process.exitCode = 1;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
fail(err);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
sponsor
|
|
497
|
+
.command("ungrant")
|
|
498
|
+
.argument("<person>", "Member email or prs_ id")
|
|
499
|
+
.description("Admin: take a member's personal plan off the company bill")
|
|
500
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
501
|
+
.option("--json", "Emit raw JSON")
|
|
502
|
+
.action(async function (person, opts) {
|
|
503
|
+
try {
|
|
504
|
+
const token = await ensureCognitoToken();
|
|
505
|
+
const named = opts.company ?? billing.opts().company;
|
|
506
|
+
const companyUid = await getCompanyUid(token, named);
|
|
507
|
+
const personUid = await resolveMemberPersonUid(token, companyUid, person);
|
|
508
|
+
const data = await callApi(token, "/v1/billing/sponsorship/plans", { method: "DELETE", body: { companyUid, personUid } });
|
|
509
|
+
if (opts.json) {
|
|
510
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
console.log(data.revoked
|
|
514
|
+
? "Ended. Their personal plan goes back to them from the next bill."
|
|
515
|
+
: "This company wasn't covering their plan. Nothing changed.");
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
fail(err);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
405
521
|
}
|
|
406
522
|
//# sourceMappingURL=billing-sponsor.js.map
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { Command } from "commander";
|
|
30
30
|
import { type BillingUsageReport, type ReadMonthUsageDeps, type StorageSplit, type UsageScope } from "../lib/billing/usage-cost.js";
|
|
31
|
-
import { type
|
|
31
|
+
import { type UsageCard } from "../lib/billing/usage-card.js";
|
|
32
|
+
import { type SharedBillingState, type PricingPlan } from "../lib/billing/usage-only.js";
|
|
32
33
|
export interface MembershipRef {
|
|
33
34
|
uid: string;
|
|
34
35
|
slug: string;
|
|
@@ -60,6 +61,11 @@ export interface BillingUsageDeps {
|
|
|
60
61
|
* for the plan when usage-limits does not carry it (older server).
|
|
61
62
|
*/
|
|
62
63
|
fetchBillingSummary?: (scope: UsageScope) => Promise<unknown | null>;
|
|
64
|
+
/**
|
|
65
|
+
* The shared billing state for the scope, read when neither the statement
|
|
66
|
+
* nor the usage-limits body carries one. Null when it is not known.
|
|
67
|
+
*/
|
|
68
|
+
readBillingState?: (scope: UsageScope) => Promise<SharedBillingState | null>;
|
|
63
69
|
now(): Date;
|
|
64
70
|
}
|
|
65
71
|
export interface BillingUsageOptions {
|
|
@@ -91,7 +97,11 @@ export declare function resolveOnUsagePricing(limitsPlan: PricingPlan | null, su
|
|
|
91
97
|
/** Thrown when the company statement is refused to a caller who is not its owner. */
|
|
92
98
|
export declare class UsageStatementForbiddenError extends Error {
|
|
93
99
|
}
|
|
94
|
-
|
|
100
|
+
/** The usage report plus the shared billing state it was read with (US-025). */
|
|
101
|
+
export type BillingUsageResult = BillingUsageReport & {
|
|
102
|
+
billingState?: SharedBillingState | null;
|
|
103
|
+
};
|
|
104
|
+
export declare function runBillingUsage(opts: BillingUsageOptions, deps: BillingUsageDeps): Promise<BillingUsageResult>;
|
|
95
105
|
/**
|
|
96
106
|
* `hq billing usage --card`: the card from GET /v1/pricing, or the bundled
|
|
97
107
|
* card when the feed has none. Needs no sign-in.
|
|
@@ -99,6 +109,10 @@ export declare function runBillingUsage(opts: BillingUsageOptions, deps: Billing
|
|
|
99
109
|
export declare function renderUsageCard(opts?: {
|
|
100
110
|
json?: boolean;
|
|
101
111
|
fetchPricing?: () => Promise<unknown | null>;
|
|
112
|
+
/** The caller's billing state, best effort. Absent or null prints the card alone. */
|
|
113
|
+
readState?: () => Promise<SharedBillingState | null>;
|
|
102
114
|
}): Promise<string>;
|
|
115
|
+
/** The text output: the report, then the account lines from the billing state. */
|
|
116
|
+
export declare function formatUsageResult(report: BillingUsageResult, card: UsageCard): string;
|
|
103
117
|
export declare function registerBillingUsageCommand(billing: Command): void;
|
|
104
118
|
//# sourceMappingURL=billing-usage.d.ts.map
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
* projected bill.
|
|
28
28
|
*/
|
|
29
29
|
import chalk from "chalk";
|
|
30
|
-
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
30
|
+
import { ensureCognitoToken, hasPersonCachedSession } from "../utils/cognito-session.js";
|
|
31
31
|
import { vaultApiFetch, vaultApiFetchPublic } from "../utils/vault-api.js";
|
|
32
32
|
import { buildReadingsReport, buildReport, formatReport, readMonthUsage, usageLimitsSource, usageStatementSource, } from "../lib/billing/usage-cost.js";
|
|
33
33
|
import { ratesFromPricingStatement } from "../lib/billing/usage-rate-card.js";
|
|
34
34
|
import { fetchUsageCard, formatUsageCardLines } from "../lib/billing/usage-card.js";
|
|
35
|
-
import {
|
|
35
|
+
import { accountStatusLines } from "../lib/billing/billing-status-lines.js";
|
|
36
|
+
import { fetchCompanyBillingSummary, pricingPlanFromBody, hasBillingStateField, readBillingState, usageTrialActiveFromBody, } from "../lib/billing/usage-only.js";
|
|
36
37
|
export class UsageScopeError extends Error {
|
|
37
38
|
}
|
|
38
39
|
/** Pick the scope. Never reaches outside the caller's own memberships. */
|
|
@@ -153,9 +154,14 @@ export async function runBillingUsage(opts, deps) {
|
|
|
153
154
|
})
|
|
154
155
|
: null;
|
|
155
156
|
const onUsagePricing = resolveOnUsagePricing(limitsPlan, pricingPlanFromSummary(summary));
|
|
157
|
+
const billingState = readBillingState(statement?.body) ??
|
|
158
|
+
readBillingState(limitsBody) ??
|
|
159
|
+
readBillingState(summary) ??
|
|
160
|
+
(deps.readBillingState ? await deps.readBillingState(scope).catch(() => null) : undefined);
|
|
161
|
+
const withState = (report) => billingState === undefined ? report : { ...report, billingState };
|
|
156
162
|
if (onUsagePricing !== true)
|
|
157
|
-
return buildReadingsReport(usage, onUsagePricing);
|
|
158
|
-
return { ...buildReport(usage, ratesFromPricingStatement(pricing)), onUsagePricing: true };
|
|
163
|
+
return withState(buildReadingsReport(usage, onUsagePricing));
|
|
164
|
+
return withState({ ...buildReport(usage, ratesFromPricingStatement(pricing)), onUsagePricing: true });
|
|
159
165
|
}
|
|
160
166
|
// ── Production deps ─────────────────────────────────────────────────────────
|
|
161
167
|
function productionDeps() {
|
|
@@ -223,6 +229,9 @@ function productionDeps() {
|
|
|
223
229
|
return null;
|
|
224
230
|
return res.json().catch(() => null);
|
|
225
231
|
},
|
|
232
|
+
async readBillingState(scope) {
|
|
233
|
+
return readBillingState(await this.fetchBillingSummary(scope));
|
|
234
|
+
},
|
|
226
235
|
now: () => new Date(),
|
|
227
236
|
};
|
|
228
237
|
}
|
|
@@ -232,12 +241,44 @@ function productionDeps() {
|
|
|
232
241
|
*/
|
|
233
242
|
export async function renderUsageCard(opts = {}) {
|
|
234
243
|
const { card, source } = await fetchUsageCard(opts.fetchPricing ? { fetchPricing: opts.fetchPricing } : {});
|
|
235
|
-
|
|
236
|
-
|
|
244
|
+
const state = opts.readState ? await opts.readState().catch(() => null) : null;
|
|
245
|
+
if (opts.json) {
|
|
246
|
+
return JSON.stringify({ source, card, ...(opts.readState ? { billingState: state } : {}) }, null, 2);
|
|
247
|
+
}
|
|
248
|
+
const account = accountStatusLines(state, card);
|
|
237
249
|
const from = source === "feed"
|
|
238
250
|
? "from GET /v1/pricing"
|
|
239
251
|
: "bundled copy; the pricing feed has no usage card yet";
|
|
240
|
-
return [
|
|
252
|
+
return [
|
|
253
|
+
`HQ usage pricing (card ${card.version}, ${from})`,
|
|
254
|
+
"",
|
|
255
|
+
...formatUsageCardLines(card),
|
|
256
|
+
...(account.length > 0 ? ["", "Your account:", ...account.map((l) => ` ${l}`)] : []),
|
|
257
|
+
].join("\n");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The billing state for `--card`, only when a sign-in is already cached: the
|
|
261
|
+
* card needs no sign-in, so this never opens a browser login. Any failure is
|
|
262
|
+
* null and the card prints alone.
|
|
263
|
+
*/
|
|
264
|
+
async function cachedSessionBillingState(company) {
|
|
265
|
+
if (!hasPersonCachedSession())
|
|
266
|
+
return null;
|
|
267
|
+
const token = await ensureCognitoToken({ interactive: false });
|
|
268
|
+
const res = await vaultApiFetch({ token, path: "/membership/me" });
|
|
269
|
+
if (!res.ok)
|
|
270
|
+
return null;
|
|
271
|
+
const { companiesFromMemberships } = await import("./storage.js");
|
|
272
|
+
const scope = resolveUsageScope(companiesFromMemberships(await res.json()), { company });
|
|
273
|
+
if (scope.kind === "company")
|
|
274
|
+
return readBillingState(await fetchCompanyBillingSummary(token, scope.companyUid));
|
|
275
|
+
const person = await vaultApiFetch({ token, path: "/v1/billing/person/summary" });
|
|
276
|
+
return person.ok ? readBillingState(await person.json().catch(() => null)) : null;
|
|
277
|
+
}
|
|
278
|
+
/** The text output: the report, then the account lines from the billing state. */
|
|
279
|
+
export function formatUsageResult(report, card) {
|
|
280
|
+
const account = accountStatusLines(report.billingState ?? null, card);
|
|
281
|
+
return account.length > 0 ? [formatReport(report), "", ...account].join("\n") : formatReport(report);
|
|
241
282
|
}
|
|
242
283
|
export function registerBillingUsageCommand(billing) {
|
|
243
284
|
billing
|
|
@@ -252,12 +293,25 @@ export function registerBillingUsageCommand(billing) {
|
|
|
252
293
|
.action(async function (opts) {
|
|
253
294
|
try {
|
|
254
295
|
if (opts.card) {
|
|
255
|
-
|
|
296
|
+
const cardCompany = opts.company ?? this.parent?.opts().company;
|
|
297
|
+
console.log(await renderUsageCard({
|
|
298
|
+
json: opts.json === true,
|
|
299
|
+
readState: () => cachedSessionBillingState(cardCompany),
|
|
300
|
+
}));
|
|
256
301
|
return;
|
|
257
302
|
}
|
|
258
303
|
const parentCompany = this.parent?.opts().company ?? undefined;
|
|
259
304
|
const report = await runBillingUsage({ ...opts, company: opts.company ?? parentCompany }, productionDeps());
|
|
260
|
-
|
|
305
|
+
if (opts.json) {
|
|
306
|
+
console.log(JSON.stringify(report, null, 2));
|
|
307
|
+
}
|
|
308
|
+
else if (report.billingState) {
|
|
309
|
+
const { card } = await fetchUsageCard();
|
|
310
|
+
console.log(formatUsageResult(report, card));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
console.log(formatReport(report));
|
|
314
|
+
}
|
|
261
315
|
}
|
|
262
316
|
catch (err) {
|
|
263
317
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -25,6 +25,17 @@
|
|
|
25
25
|
* pricing: the flat-plan checkout is closed, so the CLI never opens it.
|
|
26
26
|
*/
|
|
27
27
|
import { Command } from "commander";
|
|
28
|
+
/** Shape of `GET /v1/billing/summary` we render. */
|
|
29
|
+
interface BillingSummary {
|
|
30
|
+
subscriptionStatus: string | null;
|
|
31
|
+
defaultPaymentMethod: {
|
|
32
|
+
present: boolean;
|
|
33
|
+
brand: string | null;
|
|
34
|
+
last4: string | null;
|
|
35
|
+
};
|
|
36
|
+
databases?: DatabaseBilling | null;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
28
39
|
interface DatabaseBilling {
|
|
29
40
|
period: string;
|
|
30
41
|
activeApps: number;
|
|
@@ -37,6 +48,29 @@ interface DatabaseBilling {
|
|
|
37
48
|
}>;
|
|
38
49
|
}
|
|
39
50
|
export declare function formatDatabaseBilling(db: DatabaseBilling | null | undefined): string[];
|
|
51
|
+
/**
|
|
52
|
+
* The plan a person can act on, in words. The summary's `billingStatus.plan`
|
|
53
|
+
* is the Stripe webhook's label and says "usage" for ANY non-Workforce
|
|
54
|
+
* subscription (a metered usage subscription on a company that still prices
|
|
55
|
+
* as a flat plan reads "usage" there, hq-pro summary.ts). The plan the server
|
|
56
|
+
* actually prices by is `billingState.kind` (hq-pro #5088) or, on an older
|
|
57
|
+
* server, `pricingPlan`; `billingStatus.plan` only separates Workforce from
|
|
58
|
+
* the rest. DEV-5179: a reporter read "usage" off the status JSON while
|
|
59
|
+
* `hq billing usage` said "flat plan"; both were right about different fields.
|
|
60
|
+
*/
|
|
61
|
+
export declare function describePlan(summary: BillingSummary): string;
|
|
62
|
+
export declare const PLAN_UNAVAILABLE = "unavailable right now";
|
|
63
|
+
/**
|
|
64
|
+
* A scheduled switch to usage pricing (hq-pro US-014 `GET
|
|
65
|
+
* /v1/billing/usage-switch`), or null. The route is owner-only and absent on
|
|
66
|
+
* older servers. A refusal (403, 404) reads as "nothing scheduled"; any other
|
|
67
|
+
* failure is "unreadable" so the status view can say so instead of implying
|
|
68
|
+
* no switch. The status view never fails on it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function fetchScheduledUsageSwitch(token: string, companyUid: string): Promise<{
|
|
71
|
+
switchDate: string;
|
|
72
|
+
phase: "scheduled" | "preview";
|
|
73
|
+
} | "unreadable" | null>;
|
|
40
74
|
export declare function registerBillingCommand(program: Command): void;
|
|
41
75
|
export {};
|
|
42
76
|
//# sourceMappingURL=billing.d.ts.map
|