@indigoai-us/hq-cli 5.75.0 → 5.77.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.js +8 -3
- package/dist/commands/files.d.ts +61 -0
- package/dist/commands/files.js +274 -0
- package/dist/commands/mcp-registration.d.ts +4 -5
- package/dist/commands/mcp-registration.js +5 -4
- package/dist/commands/outposts.d.ts +20 -4
- package/dist/commands/outposts.js +79 -10
- package/dist/commands/pack-install.d.ts +14 -17
- package/dist/commands/pack-install.js +53 -29
- package/dist/commands/pkg-install.js +3 -1
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +9 -3
- package/dist/commands/secrets.js +189 -87
- package/dist/run/hq-plugin.js +94 -31
- package/dist/utils/billing-gate.d.ts +15 -0
- package/dist/utils/billing-gate.js +35 -0
- package/dist/utils/sandbox-runner-client.d.ts +1 -0
- package/dist/utils/sandbox-runner-client.js +1 -0
- package/dist/utils/secrets-cache.d.ts +4 -5
- package/dist/utils/secrets-cache.js +5 -8
- package/package.json +3 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/commands/agents.test.ts +41 -0
- package/src/commands/agents.ts +7 -3
- package/src/commands/files-recovery.test.ts +361 -0
- package/src/commands/files.ts +410 -0
- package/src/commands/mcp-registration.ts +9 -9
- package/src/commands/outposts.test.ts +155 -24
- package/src/commands/outposts.ts +199 -45
- package/src/commands/pack-install-secret-authorization.test.ts +115 -0
- package/src/commands/pack-install.test.ts +5 -1
- package/src/commands/pack-install.ts +67 -29
- package/src/commands/pkg-install.ts +3 -1
- package/src/commands/run.test.ts +45 -0
- package/src/commands/run.ts +20 -4
- package/src/commands/secrets.test.ts +366 -25
- package/src/commands/secrets.ts +222 -96
- package/src/run/hq-plugin.test.ts +186 -10
- package/src/run/hq-plugin.ts +102 -32
- package/src/utils/__fixtures__/scan-packages.generated-block.sh +23 -0
- package/src/utils/billing-gate.ts +46 -0
- package/src/utils/pack-contributions.test.ts +90 -31
- package/src/utils/sandbox-runner-client.test.ts +28 -0
- package/src/utils/sandbox-runner-client.ts +2 -0
- package/src/utils/secrets-cache.ts +5 -8
- package/test/commands/signals.test.ts +2 -2
- package/test/commands/sources.test.ts +2 -2
- package/test/helpers/vault-service-mock.ts +76 -17
- package/test/sources-signals/smoke.test.ts +2 -2
package/dist/run/hq-plugin.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { ResolutionError } from 'varlock/plugin-lib';
|
|
2
|
-
import { DEFAULT_SECRETS_CACHE_TTL_MS,
|
|
3
|
-
function normalizeCacheTtlMs(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { DEFAULT_SECRETS_CACHE_TTL_MS, writeCache, removeCacheEntry, } from '../utils/secrets-cache.js';
|
|
3
|
+
function normalizeCacheTtlMs(secret) {
|
|
4
|
+
if (secret.tier === 'sensitive' ||
|
|
5
|
+
secret.tier === 'nuclear' ||
|
|
6
|
+
secret.scriptLock?.mode === 'enforced') {
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
if (secret.cacheTtlMs === undefined) {
|
|
10
|
+
return DEFAULT_SECRETS_CACHE_TTL_MS;
|
|
11
|
+
}
|
|
12
|
+
return typeof secret.cacheTtlMs === 'number' &&
|
|
13
|
+
Number.isFinite(secret.cacheTtlMs) &&
|
|
14
|
+
secret.cacheTtlMs > 0
|
|
15
|
+
? secret.cacheTtlMs
|
|
16
|
+
: 0;
|
|
7
17
|
}
|
|
8
18
|
export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
9
19
|
const pluginState = {
|
|
@@ -41,9 +51,9 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
|
41
51
|
impliesSensitive: true,
|
|
42
52
|
argsSchema: { type: 'array', arrayMaxLength: 1 },
|
|
43
53
|
resolve: async function () {
|
|
44
|
-
//
|
|
45
|
-
// `prewarmHqSecrets(graph, opts, state)`
|
|
46
|
-
//
|
|
54
|
+
// `pluginState` is captured by this inner-class closure;
|
|
55
|
+
// `prewarmHqSecrets(graph, opts, state)` server-authorizes and populates
|
|
56
|
+
// the in-memory values before `graph.resolveEnvValues()` calls us.
|
|
47
57
|
const explicit = this.arrArgs?.[0]?.staticValue;
|
|
48
58
|
const secretName = (typeof explicit === 'string' && explicit) ? explicit : this._ownerKey;
|
|
49
59
|
if (!secretName) {
|
|
@@ -66,11 +76,6 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
|
66
76
|
}
|
|
67
77
|
throw new ResolutionError(`Failed to load secret "${secretName}": ${err.message ?? err.code}`);
|
|
68
78
|
}
|
|
69
|
-
// Sentinel-check style throughout: `readCache` returns `string | null`
|
|
70
|
-
// (verified at `hq/packages/hq-cli/src/utils/secrets-cache.ts:45`); `pluginState.uid`
|
|
71
|
-
// is `string | null` per `PluginState`. Use `== null` (covers null AND undefined defensively)
|
|
72
|
-
// for both — do not mix in truthy checks like `if (!x)`, which would silently swallow a
|
|
73
|
-
// legitimate empty-string value if the contract ever loosened.
|
|
74
79
|
if (pluginState.uid == null) {
|
|
75
80
|
throw new ResolutionError('Internal error: prewarmHqSecrets was not called before resolveEnvValues');
|
|
76
81
|
}
|
|
@@ -78,11 +83,7 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
|
78
83
|
if (inMemory != null) {
|
|
79
84
|
return inMemory;
|
|
80
85
|
}
|
|
81
|
-
|
|
82
|
-
if (cached == null) {
|
|
83
|
-
throw new ResolutionError(`Internal error: pre-warm did not populate cache for "${secretName}"`);
|
|
84
|
-
}
|
|
85
|
-
return cached;
|
|
86
|
+
throw new ResolutionError(`Secret "${secretName}" was not returned by vault after server authorization`);
|
|
86
87
|
},
|
|
87
88
|
};
|
|
88
89
|
// Captured during process(parent); used by resolve() to fall back to the var key.
|
|
@@ -144,22 +145,84 @@ export async function prewarmHqSecrets(graph /* EnvGraph */, opts, state) {
|
|
|
144
145
|
if (uniqueNames.length > 100) {
|
|
145
146
|
throw new Error(`hq run supports at most 100 hq() resolvers per schema; got ${uniqueNames.length}`);
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
state.loadedSecretsByName.clear();
|
|
149
|
+
state.errorsByName = new Map();
|
|
150
|
+
let result;
|
|
151
|
+
try {
|
|
152
|
+
result = await opts.fetchBatch(uid, uniqueNames, opts.usage);
|
|
153
|
+
if (!Array.isArray(result.secrets) || !Array.isArray(result.errors)) {
|
|
154
|
+
throw new Error('Invalid secret load response from vault');
|
|
151
155
|
}
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
const errorsByName = new Map();
|
|
157
|
+
const returnedNames = new Set();
|
|
158
|
+
const requestedNames = new Set(uniqueNames);
|
|
159
|
+
const seenNames = new Set();
|
|
160
|
+
for (const rawSecret of result.secrets) {
|
|
161
|
+
if (!rawSecret || typeof rawSecret !== 'object') {
|
|
162
|
+
throw new Error('Invalid secret load response from vault');
|
|
163
|
+
}
|
|
164
|
+
const s = rawSecret;
|
|
165
|
+
if (typeof s.name !== 'string' ||
|
|
166
|
+
!requestedNames.has(s.name) ||
|
|
167
|
+
seenNames.has(s.name) ||
|
|
168
|
+
(s.value != null && typeof s.value !== 'string')) {
|
|
169
|
+
throw new Error('Invalid secret load response from vault');
|
|
170
|
+
}
|
|
171
|
+
seenNames.add(s.name);
|
|
172
|
+
if (s.value == null) {
|
|
173
|
+
errorsByName.set(s.name, {
|
|
174
|
+
code: 'not_returned',
|
|
175
|
+
message: 'not returned by vault after server authorization',
|
|
176
|
+
});
|
|
177
|
+
removeCacheEntry(uid, s.name);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
returnedNames.add(s.name);
|
|
181
|
+
state.loadedSecretsByName.set(s.name, s.value);
|
|
182
|
+
const cacheTtlMs = normalizeCacheTtlMs(s);
|
|
183
|
+
if (cacheTtlMs > 0) {
|
|
184
|
+
writeCache(uid, s.name, s.value, cacheTtlMs);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
removeCacheEntry(uid, s.name);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const rawError of result.errors) {
|
|
191
|
+
if (!rawError || typeof rawError !== 'object') {
|
|
192
|
+
throw new Error('Invalid secret load response from vault');
|
|
193
|
+
}
|
|
194
|
+
const e = rawError;
|
|
195
|
+
if (typeof e.name !== 'string' ||
|
|
196
|
+
!requestedNames.has(e.name) ||
|
|
197
|
+
seenNames.has(e.name) ||
|
|
198
|
+
typeof e.code !== 'string' ||
|
|
199
|
+
(e.message !== undefined && typeof e.message !== 'string')) {
|
|
200
|
+
throw new Error('Invalid secret load response from vault');
|
|
201
|
+
}
|
|
202
|
+
seenNames.add(e.name);
|
|
203
|
+
errorsByName.set(e.name, { code: e.code, message: e.message });
|
|
204
|
+
state.loadedSecretsByName.delete(e.name);
|
|
205
|
+
removeCacheEntry(uid, e.name);
|
|
156
206
|
}
|
|
207
|
+
for (const name of uniqueNames) {
|
|
208
|
+
if (!returnedNames.has(name) && !errorsByName.has(name)) {
|
|
209
|
+
errorsByName.set(name, {
|
|
210
|
+
code: 'not_returned',
|
|
211
|
+
message: 'not returned by vault after server authorization',
|
|
212
|
+
});
|
|
213
|
+
removeCacheEntry(uid, name);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
state.errorsByName = errorsByName;
|
|
217
|
+
state.uid = uid;
|
|
157
218
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
errorsByName
|
|
219
|
+
catch (err) {
|
|
220
|
+
state.loadedSecretsByName.clear();
|
|
221
|
+
state.errorsByName = new Map();
|
|
222
|
+
for (const name of uniqueNames) {
|
|
223
|
+
removeCacheEntry(uid, name);
|
|
224
|
+
}
|
|
225
|
+
throw err;
|
|
161
226
|
}
|
|
162
|
-
state.errorsByName = errorsByName;
|
|
163
|
-
state.uid = uid;
|
|
164
227
|
}
|
|
165
228
|
//# sourceMappingURL=hq-plugin.js.map
|
|
@@ -74,4 +74,19 @@ export declare function mintPaymentLink(token: string, setup: BillingSetupAction
|
|
|
74
74
|
* to act on. Never prints tokens or secrets.
|
|
75
75
|
*/
|
|
76
76
|
export declare function surfaceBillingRequired(token: string, billing: BillingErrorPayload): Promise<string | null>;
|
|
77
|
+
/**
|
|
78
|
+
* Status-aware surface for a 402 billing block. hq-pro's envelope carries two
|
|
79
|
+
* distinct remediations that must never be conflated (mirroring the server's
|
|
80
|
+
* own P1-C classification):
|
|
81
|
+
* - `payment_failed` — a card EXISTS and the charge was DECLINED. The
|
|
82
|
+
* server's `message` already carries the friendly decline copy ("Your
|
|
83
|
+
* card was declined…", "insufficient funds", …). Telling this user
|
|
84
|
+
* "No card on file" sends them down the wrong remediation path entirely
|
|
85
|
+
* (observed live 2026-07-20: a declined $80 Outpost proration surfaced
|
|
86
|
+
* as "no card", triggering a hunt for a missing card that existed).
|
|
87
|
+
* The capture link still surfaces — as the way to UPDATE the card.
|
|
88
|
+
* - anything else (`billing_required`) — genuinely no usable card on
|
|
89
|
+
* file; the existing add-a-card copy is correct.
|
|
90
|
+
*/
|
|
91
|
+
export declare function surfaceBillingBlocked(token: string, billing: BillingErrorPayload, serverMessage?: string): Promise<string | null>;
|
|
77
92
|
//# sourceMappingURL=billing-gate.d.ts.map
|
|
@@ -121,4 +121,39 @@ export async function surfaceBillingRequired(token, billing) {
|
|
|
121
121
|
console.log(chalk.dim("Once a card is added, re-run the same command."));
|
|
122
122
|
return url;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Status-aware surface for a 402 billing block. hq-pro's envelope carries two
|
|
126
|
+
* distinct remediations that must never be conflated (mirroring the server's
|
|
127
|
+
* own P1-C classification):
|
|
128
|
+
* - `payment_failed` — a card EXISTS and the charge was DECLINED. The
|
|
129
|
+
* server's `message` already carries the friendly decline copy ("Your
|
|
130
|
+
* card was declined…", "insufficient funds", …). Telling this user
|
|
131
|
+
* "No card on file" sends them down the wrong remediation path entirely
|
|
132
|
+
* (observed live 2026-07-20: a declined $80 Outpost proration surfaced
|
|
133
|
+
* as "no card", triggering a hunt for a missing card that existed).
|
|
134
|
+
* The capture link still surfaces — as the way to UPDATE the card.
|
|
135
|
+
* - anything else (`billing_required`) — genuinely no usable card on
|
|
136
|
+
* file; the existing add-a-card copy is correct.
|
|
137
|
+
*/
|
|
138
|
+
export async function surfaceBillingBlocked(token, billing, serverMessage) {
|
|
139
|
+
if (billing.status !== "payment_failed") {
|
|
140
|
+
return surfaceBillingRequired(token, billing);
|
|
141
|
+
}
|
|
142
|
+
console.error(chalk.yellow(serverMessage?.trim() ||
|
|
143
|
+
"Your payment failed. Try a different card or contact your bank."));
|
|
144
|
+
if (!billing.setup)
|
|
145
|
+
return null;
|
|
146
|
+
try {
|
|
147
|
+
const url = await mintPaymentLink(token, billing.setup);
|
|
148
|
+
console.log("Update or replace the card here (safe to share with whoever owns billing):\n " +
|
|
149
|
+
chalk.cyan(url));
|
|
150
|
+
console.log(chalk.dim("Once the payment method is sorted, re-run the same command."));
|
|
151
|
+
return url;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Link minting is best-effort on the decline path — the decline reason
|
|
155
|
+
// above is the essential part; the console billing page also works.
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
124
159
|
//# sourceMappingURL=billing-gate.js.map
|
|
@@ -36,6 +36,7 @@ function normalizeJob(body, jobIdFallback) {
|
|
|
36
36
|
: jobIdFallback ?? requireString(body, "jobId"),
|
|
37
37
|
status,
|
|
38
38
|
output: typeof body.output === "string" ? body.output : undefined,
|
|
39
|
+
error: typeof body.error === "string" ? body.error : undefined,
|
|
39
40
|
exitCode: typeof body.exitCode === "number" ? body.exitCode : undefined,
|
|
40
41
|
success: typeof body.success === "boolean" ? body.success : undefined,
|
|
41
42
|
};
|
|
@@ -3,11 +3,10 @@ export declare function readCache(companyUid: string, name: string): string | nu
|
|
|
3
3
|
export declare function writeCache(companyUid: string, name: string, value: string, ttlMs?: number): void;
|
|
4
4
|
/**
|
|
5
5
|
* List the scope UIDs (`cmp_*` / `prs_*` subdirectories) that currently have a
|
|
6
|
-
* secrets-cache directory on disk.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* unreadable (the desired graceful-deferral behavior — no scopes, no hits).
|
|
6
|
+
* secrets-cache directory on disk. Install-time MCP registration may use an
|
|
7
|
+
* exactly-one result as a scope hint before reauthorizing every value online; it
|
|
8
|
+
* never reads cached plaintext through this helper. Returns `[]` when the cache
|
|
9
|
+
* root is absent or unreadable.
|
|
11
10
|
*/
|
|
12
11
|
export declare function listSecretCacheScopes(): string[];
|
|
13
12
|
export declare function removeCacheEntry(companyUid: string, name: string): void;
|
|
@@ -140,11 +140,10 @@ export function writeCache(companyUid, name, value, ttlMs = DEFAULT_SECRETS_CACH
|
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
142
|
* List the scope UIDs (`cmp_*` / `prs_*` subdirectories) that currently have a
|
|
143
|
-
* secrets-cache directory on disk.
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* unreadable (the desired graceful-deferral behavior — no scopes, no hits).
|
|
143
|
+
* secrets-cache directory on disk. Install-time MCP registration may use an
|
|
144
|
+
* exactly-one result as a scope hint before reauthorizing every value online; it
|
|
145
|
+
* never reads cached plaintext through this helper. Returns `[]` when the cache
|
|
146
|
+
* root is absent or unreadable.
|
|
148
147
|
*/
|
|
149
148
|
export function listSecretCacheScopes() {
|
|
150
149
|
try {
|
|
@@ -152,9 +151,7 @@ export function listSecretCacheScopes() {
|
|
|
152
151
|
.readdirSync(CACHE_DIR, { withFileTypes: true })
|
|
153
152
|
.filter((e) => e.isDirectory())
|
|
154
153
|
.map((e) => e.name)
|
|
155
|
-
|
|
156
|
-
// rejects anything with `/` or `..`, so this is belt-and-suspenders.
|
|
157
|
-
.filter((name) => !name.startsWith("."));
|
|
154
|
+
.filter((name) => /^(?:cmp|prs)_[A-Za-z0-9_-]+$/.test(name));
|
|
158
155
|
}
|
|
159
156
|
catch {
|
|
160
157
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.77.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"build": "node scripts/generate-dsn.mjs && tsc && node scripts/chmod-bins.mjs",
|
|
12
12
|
"prepublishOnly": "npm run build",
|
|
13
13
|
"typecheck": "tsc --noEmit",
|
|
14
|
+
"gen:scan-golden": "node scripts/generate-scan-packages-table.mjs > src/utils/__fixtures__/scan-packages.generated-block.sh",
|
|
14
15
|
"lint": "eslint .",
|
|
15
16
|
"test": "vitest run",
|
|
16
17
|
"test:db": "vitest run src/lib/db test/commands/db.test.ts test/commands/db-tenant-isolation.test.ts",
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
24
|
-
"@indigoai-us/hq-cloud": "^6.14.
|
|
25
|
+
"@indigoai-us/hq-cloud": "^6.14.14",
|
|
25
26
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
26
27
|
"@sentry/node": "^10.49.0",
|
|
27
28
|
"better-sqlite3": "^12.11.1",
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -363,6 +363,47 @@ describe("hq agents provision (billing gate)", () => {
|
|
|
363
363
|
expect(printed).toContain("https://checkout.stripe.com/card");
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
+
it("surfaces the DECLINE reason on 402 payment_failed (message-first decoding)", async () => {
|
|
367
|
+
// The payment_failed envelope carries BOTH the generic error AND the
|
|
368
|
+
// friendly decline message — the decode must prefer `message` or the
|
|
369
|
+
// decline copy is lost and the operator sees "add a card" for a card
|
|
370
|
+
// that exists and was declined.
|
|
371
|
+
fetchSpy
|
|
372
|
+
.mockResolvedValueOnce(
|
|
373
|
+
jsonResponse(402, {
|
|
374
|
+
error: "payment required",
|
|
375
|
+
message:
|
|
376
|
+
"Your card was declined. Try a different card or contact your bank.",
|
|
377
|
+
code: "PAYMENT_FAILED",
|
|
378
|
+
billing: {
|
|
379
|
+
status: "payment_failed",
|
|
380
|
+
setup: {
|
|
381
|
+
payerType: "company",
|
|
382
|
+
path: "/v1/billing/checkout/org",
|
|
383
|
+
method: "POST",
|
|
384
|
+
body: { companyUid: "cmp_acme" },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
)
|
|
389
|
+
.mockResolvedValueOnce(
|
|
390
|
+
jsonResponse(200, { url: "https://checkout.stripe.com/update" }),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const logSpyLocal = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
394
|
+
const errSpyLocal = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
395
|
+
await expect(
|
|
396
|
+
run(["agents", "--company", "acme", "provision", "Ops Bot", "--yes"]),
|
|
397
|
+
).rejects.toThrow("process.exit(1)");
|
|
398
|
+
|
|
399
|
+
const printed = [...errSpyLocal.mock.calls, ...logSpyLocal.mock.calls]
|
|
400
|
+
.map((c) => c.map(String).join(" "))
|
|
401
|
+
.join("\n");
|
|
402
|
+
expect(printed).toContain("declined");
|
|
403
|
+
expect(printed).not.toContain("No card on file");
|
|
404
|
+
expect(printed).toContain("https://checkout.stripe.com/update");
|
|
405
|
+
});
|
|
406
|
+
|
|
366
407
|
it("requires --api-key-env for --auth-mode apiKey (before any charge)", async () => {
|
|
367
408
|
await expect(
|
|
368
409
|
run([
|
package/src/commands/agents.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
AGENT_PRICE_CENTS,
|
|
32
32
|
confirmChargeOrExit,
|
|
33
33
|
parseBillingPayload,
|
|
34
|
-
|
|
34
|
+
surfaceBillingBlocked,
|
|
35
35
|
type BillingErrorPayload,
|
|
36
36
|
} from "../utils/billing-gate.js";
|
|
37
37
|
|
|
@@ -125,7 +125,11 @@ export async function agentsRequest<T>(opts: {
|
|
|
125
125
|
};
|
|
126
126
|
throw new AgentsHttpError(
|
|
127
127
|
res.status,
|
|
128
|
-
|
|
128
|
+
// `message` FIRST: on a payment_failed envelope it carries the friendly
|
|
129
|
+
// decline copy ("Your card was declined…") while `error` is the generic
|
|
130
|
+
// "payment required" — error-first would feed surfaceBillingBlocked the
|
|
131
|
+
// generic string and lose the decline reason (mirrors outpostRequest).
|
|
132
|
+
body.message ?? body.error ?? res.statusText,
|
|
129
133
|
body.code,
|
|
130
134
|
parseBillingPayload(body),
|
|
131
135
|
);
|
|
@@ -592,7 +596,7 @@ export function registerAgentsCommand(program: Command): void {
|
|
|
592
596
|
err.status === 402 &&
|
|
593
597
|
err.billing
|
|
594
598
|
) {
|
|
595
|
-
await
|
|
599
|
+
await surfaceBillingBlocked(token, err.billing, err.message);
|
|
596
600
|
process.exit(1);
|
|
597
601
|
}
|
|
598
602
|
throw err;
|