@indigoai-us/hq-cli 5.62.1 → 5.63.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/dist/commands/agents.d.ts +22 -1
- package/dist/commands/agents.js +98 -4
- package/dist/commands/billing.d.ts +17 -0
- package/dist/commands/billing.js +114 -0
- package/dist/commands/cloud.d.ts +11 -0
- package/dist/commands/cloud.js +40 -2
- package/dist/commands/members.d.ts +12 -1
- package/dist/commands/members.js +79 -12
- package/dist/commands/outposts.d.ts +18 -1
- package/dist/commands/outposts.js +91 -4
- package/dist/main.js +7 -2
- package/dist/utils/billing-gate.d.ts +77 -0
- package/dist/utils/billing-gate.js +127 -0
- package/package.json +2 -2
- package/src/commands/agents.test.ts +84 -0
- package/src/commands/agents.ts +157 -1
- package/src/commands/billing.test.ts +158 -0
- package/src/commands/billing.ts +146 -0
- package/src/commands/cloud.pull-all.test.ts +53 -0
- package/src/commands/cloud.ts +53 -0
- package/src/commands/members.test.ts +78 -0
- package/src/commands/members.ts +108 -11
- package/src/commands/outposts.test.ts +70 -0
- package/src/commands/outposts.ts +128 -2
- package/src/main.ts +6 -0
- package/src/utils/billing-gate.test.ts +95 -0
- package/src/utils/billing-gate.ts +182 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing gate for paid provisioning (agents & Outposts).
|
|
3
|
+
*
|
|
4
|
+
* hq-pro enforces billing server-side: a provision call for a resource whose
|
|
5
|
+
* payer has no card on file returns `402` with a `billing` payload carrying a
|
|
6
|
+
* `setup` action that names EXACTLY which checkout endpoint to hit. That
|
|
7
|
+
* endpoint returns `{ url }` — a Stripe-hosted card-capture link.
|
|
8
|
+
*
|
|
9
|
+
* This module wires the client-side experience around that contract:
|
|
10
|
+
* 1. `confirmChargeOrExit` — print the monthly cost and require `--yes`
|
|
11
|
+
* BEFORE any paid provisioning call is made.
|
|
12
|
+
* 2. `surfaceBillingRequired` — when the backend answers `402 billing_required`,
|
|
13
|
+
* mint the card-capture link and print a plain, shareable message instead
|
|
14
|
+
* of an opaque failure.
|
|
15
|
+
* 3. `hq billing checkout` (see commands/billing.ts) reuses `mintPaymentLink`
|
|
16
|
+
* to hand out the same link proactively.
|
|
17
|
+
*
|
|
18
|
+
* Prices mirror the console's authoritative constants
|
|
19
|
+
* (hq-console `src/lib/billing-pricing.ts`): amounts are in the smallest
|
|
20
|
+
* currency unit (US cents) to match Stripe's wire shape.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import chalk from "chalk";
|
|
24
|
+
import { vaultApiFetch } from "./vault-api.js";
|
|
25
|
+
|
|
26
|
+
/** Per-Agent monthly list price, in cents ($100.00 / agent / month). */
|
|
27
|
+
export const AGENT_PRICE_CENTS = 10_000;
|
|
28
|
+
/** Per-Outpost monthly list price, in cents ($80.00 / Outpost / month). */
|
|
29
|
+
export const OUTPOST_PRICE_CENTS = 8_000;
|
|
30
|
+
|
|
31
|
+
/** Render a cents amount as a `$X.YZ` USD string. */
|
|
32
|
+
export function formatUsd(cents: number): string {
|
|
33
|
+
return `$${(Math.max(0, Math.round(cents)) / 100).toFixed(2)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The server-supplied "how to add a card" action on a `402 billing_required`.
|
|
38
|
+
* Mirrors hq-pro `src/billing/activation-billing.ts` `BillingSetupAction` and
|
|
39
|
+
* the console's `src/lib/billing-setup.ts`.
|
|
40
|
+
*/
|
|
41
|
+
export interface BillingSetupAction {
|
|
42
|
+
payerType: "company" | "person";
|
|
43
|
+
path: "/v1/billing/checkout/org" | "/v1/billing/checkout/person";
|
|
44
|
+
method: "POST";
|
|
45
|
+
body?: { companyUid?: string };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The `billing` envelope hq-pro attaches to a billing-blocked provision. */
|
|
49
|
+
export interface BillingErrorPayload {
|
|
50
|
+
status: string;
|
|
51
|
+
setup?: BillingSetupAction;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Narrow an unknown value to a usable `BillingSetupAction`. */
|
|
55
|
+
export function isBillingSetupAction(value: unknown): value is BillingSetupAction {
|
|
56
|
+
if (!value || typeof value !== "object") return false;
|
|
57
|
+
const setup = value as Partial<BillingSetupAction>;
|
|
58
|
+
const body = setup.body;
|
|
59
|
+
return (
|
|
60
|
+
(setup.payerType === "company" || setup.payerType === "person") &&
|
|
61
|
+
(setup.path === "/v1/billing/checkout/org" ||
|
|
62
|
+
setup.path === "/v1/billing/checkout/person") &&
|
|
63
|
+
setup.method === "POST" &&
|
|
64
|
+
(body === undefined || (typeof body === "object" && body !== null))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Pull a `BillingErrorPayload` out of a decoded hq-pro error body, if present. */
|
|
69
|
+
export function parseBillingPayload(body: unknown): BillingErrorPayload | undefined {
|
|
70
|
+
if (!body || typeof body !== "object") return undefined;
|
|
71
|
+
const raw = (body as { billing?: unknown }).billing;
|
|
72
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
73
|
+
const status = (raw as { status?: unknown }).status;
|
|
74
|
+
if (typeof status !== "string") return undefined;
|
|
75
|
+
const setup = (raw as { setup?: unknown }).setup;
|
|
76
|
+
return {
|
|
77
|
+
status,
|
|
78
|
+
...(isBillingSetupAction(setup) ? { setup } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type PaidResource = "agent" | "Outpost";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gate a paid provisioning call behind an explicit, informed `--yes`. Prints the
|
|
86
|
+
* monthly charge in full prose (this is a paid, outward action — Auto-Clarity)
|
|
87
|
+
* and exits non-zero when the caller has not passed `--yes`. Returns normally
|
|
88
|
+
* once approved so the caller proceeds to provision.
|
|
89
|
+
*/
|
|
90
|
+
export function confirmChargeOrExit(opts: {
|
|
91
|
+
resource: PaidResource;
|
|
92
|
+
unitCents: number;
|
|
93
|
+
quantity?: number;
|
|
94
|
+
yes?: boolean;
|
|
95
|
+
}): void {
|
|
96
|
+
const quantity = opts.quantity ?? 1;
|
|
97
|
+
const total = opts.unitCents * quantity;
|
|
98
|
+
const each =
|
|
99
|
+
quantity > 1 ? ` (${formatUsd(opts.unitCents)} each × ${quantity})` : "";
|
|
100
|
+
const noun = quantity > 1 ? `${opts.resource}s` : opts.resource;
|
|
101
|
+
const article = /^[aeiou]/i.test(opts.resource) ? "an" : "a";
|
|
102
|
+
const subject = quantity > 1 ? `${quantity} ${noun}` : `${article} ${noun}`;
|
|
103
|
+
if (!opts.yes) {
|
|
104
|
+
console.error(
|
|
105
|
+
chalk.yellow(
|
|
106
|
+
`Provisioning ${subject} adds a recurring charge of ` +
|
|
107
|
+
`${formatUsd(total)}/month${each} to the payer on file. This is a ` +
|
|
108
|
+
`paid resource.\n` +
|
|
109
|
+
`Re-run with --yes to confirm you want to incur this charge.`,
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
console.log(
|
|
115
|
+
chalk.dim(
|
|
116
|
+
`Confirmed: ${formatUsd(total)}/month for ${quantity} ${noun}. Provisioning…`,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Mint a Stripe-hosted card-capture link for a `BillingSetupAction`. Returns the
|
|
123
|
+
* hosted URL the payer opens to add a card. Throws on a non-2xx (the caller
|
|
124
|
+
* decides how to surface it) — never swallows.
|
|
125
|
+
*/
|
|
126
|
+
export async function mintPaymentLink(
|
|
127
|
+
token: string,
|
|
128
|
+
setup: BillingSetupAction,
|
|
129
|
+
): Promise<string> {
|
|
130
|
+
const res = await vaultApiFetch({
|
|
131
|
+
token,
|
|
132
|
+
path: setup.path,
|
|
133
|
+
method: "POST",
|
|
134
|
+
body: setup.body?.companyUid ? { companyUid: setup.body.companyUid } : {},
|
|
135
|
+
});
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
138
|
+
error?: string;
|
|
139
|
+
message?: string;
|
|
140
|
+
};
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Failed to mint a payment link: ${body.error ?? body.message ?? res.statusText}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const data = (await res.json()) as { url?: string };
|
|
146
|
+
if (!data.url) {
|
|
147
|
+
throw new Error("Payment link response did not include a checkout URL.");
|
|
148
|
+
}
|
|
149
|
+
return data.url;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The provision was blocked because the payer has no card on file. Mint the
|
|
154
|
+
* card-capture link the backend pointed us at and print a plain, shareable
|
|
155
|
+
* message so the user can add a card (or hand the link to whoever owns billing)
|
|
156
|
+
* and re-run. Returns the URL, or `null` when the server gave no `setup` action
|
|
157
|
+
* to act on. Never prints tokens or secrets.
|
|
158
|
+
*/
|
|
159
|
+
export async function surfaceBillingRequired(
|
|
160
|
+
token: string,
|
|
161
|
+
billing: BillingErrorPayload,
|
|
162
|
+
): Promise<string | null> {
|
|
163
|
+
if (!billing.setup) {
|
|
164
|
+
console.error(
|
|
165
|
+
chalk.yellow(
|
|
166
|
+
"Provisioning is blocked on billing, but the server didn't return a " +
|
|
167
|
+
"payment link. Add a card in the console billing page, then re-run.",
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const url = await mintPaymentLink(token, billing.setup);
|
|
173
|
+
console.log(
|
|
174
|
+
chalk.yellow("No card on file — provisioning is blocked until one is added."),
|
|
175
|
+
);
|
|
176
|
+
console.log(
|
|
177
|
+
"Add a card here (safe to share with whoever owns billing):\n " +
|
|
178
|
+
chalk.cyan(url),
|
|
179
|
+
);
|
|
180
|
+
console.log(chalk.dim("Once a card is added, re-run the same command."));
|
|
181
|
+
return url;
|
|
182
|
+
}
|