@myapihq/sdk 2.0.1 → 2.2.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/auth.d.ts CHANGED
@@ -32,11 +32,17 @@ export interface AuthUsage {
32
32
  period: string;
33
33
  active_users: number;
34
34
  price_cents_each: number;
35
+ as_of?: string;
35
36
  }
36
37
  export interface AuthDomain {
37
38
  domain: string;
38
- status: 'pending' | 'active';
39
- dns: {
39
+ status: 'awaiting_verification' | 'pending' | 'active';
40
+ verification?: {
41
+ type: string;
42
+ name: string;
43
+ value: string;
44
+ };
45
+ dns?: {
40
46
  type: string;
41
47
  name: string;
42
48
  value: string;
@@ -49,9 +55,15 @@ export declare function createTenant(apiKey: string, orgId: string, input?: Crea
49
55
  export declare function getTenant(apiKey: string, orgId: string): Promise<Tenant>;
50
56
  export declare function createClient(apiKey: string, orgId: string, input: CreateClientInput): Promise<AuthClient>;
51
57
  export declare function listClients(apiKey: string, orgId: string): Promise<ListClientsResponse>;
58
+ export declare function deleteClient(apiKey: string, orgId: string, clientId: string): Promise<{
59
+ client_id: string;
60
+ deleted: boolean;
61
+ }>;
62
+ export declare function rotateClient(apiKey: string, orgId: string, clientId: string): Promise<AuthClient>;
52
63
  export declare function getUsage(apiKey: string, orgId: string): Promise<AuthUsage>;
53
64
  export declare function registerDomain(apiKey: string, orgId: string, domain: string): Promise<AuthDomain>;
54
65
  export declare function getDomain(apiKey: string, orgId: string): Promise<AuthDomain>;
66
+ export declare function verifyDomain(apiKey: string, orgId: string): Promise<AuthDomain>;
55
67
  export declare function deleteDomain(apiKey: string, orgId: string, domain: string): Promise<{
56
68
  domain: string;
57
69
  status: string;
package/dist/auth.js CHANGED
@@ -5,9 +5,12 @@ exports.createTenant = createTenant;
5
5
  exports.getTenant = getTenant;
6
6
  exports.createClient = createClient;
7
7
  exports.listClients = listClients;
8
+ exports.deleteClient = deleteClient;
9
+ exports.rotateClient = rotateClient;
8
10
  exports.getUsage = getUsage;
9
11
  exports.registerDomain = registerDomain;
10
12
  exports.getDomain = getDomain;
13
+ exports.verifyDomain = verifyDomain;
11
14
  exports.deleteDomain = deleteDomain;
12
15
  const client_1 = require("./client");
13
16
  const config_1 = require("./config");
@@ -29,9 +32,12 @@ exports.EXPOSES = [
29
32
  'GET /auth/orgs/{org_id}/tenant',
30
33
  'POST /auth/orgs/{org_id}/clients',
31
34
  'GET /auth/orgs/{org_id}/clients',
35
+ 'DELETE /auth/orgs/{org_id}/clients/{client_id}',
36
+ 'POST /auth/orgs/{org_id}/clients/{client_id}/rotate',
32
37
  'GET /auth/orgs/{org_id}/usage',
33
38
  'POST /auth/orgs/{org_id}/domain',
34
39
  'GET /auth/orgs/{org_id}/domain',
40
+ 'POST /auth/orgs/{org_id}/domain/verify',
35
41
  'DELETE /auth/orgs/{org_id}/domain',
36
42
  ];
37
43
  // Create (or update) the org's auth tenant. Idempotent. `connections` selects
@@ -57,6 +63,17 @@ async function createClient(apiKey, orgId, input) {
57
63
  async function listClients(apiKey, orgId) {
58
64
  return (0, client_1.request)('GET', `${config_1.AUTH_BASE}/auth/orgs/${encodeURIComponent(orgId)}/clients`, apiKey);
59
65
  }
66
+ // Delete (revoke) an OIDC client. Irreversible — the client_id stops
67
+ // authenticating immediately.
68
+ async function deleteClient(apiKey, orgId, clientId) {
69
+ return (0, client_1.request)('DELETE', `${config_1.AUTH_BASE}/auth/orgs/${encodeURIComponent(orgId)}/clients/${encodeURIComponent(clientId)}`, apiKey);
70
+ }
71
+ // Rotate a confidential ('web') client's secret. Returns the NEW secret exactly
72
+ // once; the previous secret stops working immediately. (SPA clients have no
73
+ // secret — rotating one is rejected by the backend.)
74
+ async function rotateClient(apiKey, orgId, clientId) {
75
+ return (0, client_1.request)('POST', `${config_1.AUTH_BASE}/auth/orgs/${encodeURIComponent(orgId)}/clients/${encodeURIComponent(clientId)}/rotate`, apiKey);
76
+ }
60
77
  // Monthly-active-user usage for the current period (auth is billed per MAU).
61
78
  async function getUsage(apiKey, orgId) {
62
79
  return (0, client_1.request)('GET', `${config_1.AUTH_BASE}/auth/orgs/${encodeURIComponent(orgId)}/usage`, apiKey);
@@ -71,6 +88,13 @@ async function registerDomain(apiKey, orgId, domain) {
71
88
  async function getDomain(apiKey, orgId) {
72
89
  return (0, client_1.request)('GET', `${config_1.AUTH_BASE}/auth/orgs/${encodeURIComponent(orgId)}/domain`, apiKey);
73
90
  }
91
+ // Verify ownership of the org's custom auth domain by checking the published
92
+ // TXT challenge. On success the domain advances awaiting_verification → pending
93
+ // (and the response carries the A record). Rejects with 422 DOMAIN_NOT_VERIFIED
94
+ // if the TXT record isn't found yet. Idempotent once already pending/active.
95
+ async function verifyDomain(apiKey, orgId) {
96
+ return (0, client_1.request)('POST', `${config_1.AUTH_BASE}/auth/orgs/${encodeURIComponent(orgId)}/domain/verify`, apiKey);
97
+ }
74
98
  async function deleteDomain(apiKey, orgId, domain) {
75
99
  return (0, client_1.request)('DELETE', `${config_1.AUTH_BASE}/auth/orgs/${encodeURIComponent(orgId)}/domain`, apiKey, { domain });
76
100
  }
@@ -0,0 +1,11 @@
1
+ import { MyApiError } from './client';
2
+ export type AutoRechargeState = 'in_flight' | 'disabled' | 'capped' | 'no_pm' | 'failed';
3
+ export declare function isInsufficientFunds(err: unknown): err is MyApiError;
4
+ export declare function isSpendCapExceeded(err: unknown): err is MyApiError;
5
+ export declare function autoRechargeState(err: unknown): AutoRechargeState | undefined;
6
+ export interface FundsRetryOptions {
7
+ maxRetries?: number;
8
+ sleep?: (ms: number) => Promise<void>;
9
+ onRetry?: (attempt: number, waitSeconds: number) => void;
10
+ }
11
+ export declare function withFundsRetry<T>(call: () => Promise<T>, opts?: FundsRetryOptions): Promise<T>;
package/dist/funds.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isInsufficientFunds = isInsufficientFunds;
4
+ exports.isSpendCapExceeded = isSpendCapExceeded;
5
+ exports.autoRechargeState = autoRechargeState;
6
+ exports.withFundsRetry = withFundsRetry;
7
+ const client_1 = require("./client");
8
+ // Wallet empty. Matches the unified `INSUFFICIENT_FUNDS` and the legacy
9
+ // `INSUFFICIENT_BALANCE` (still emitted by imagegen's free-tier path until the
10
+ // backend finishes the split — tolerate both).
11
+ function isInsufficientFunds(err) {
12
+ return err instanceof client_1.MyApiError &&
13
+ (err.code === 'INSUFFICIENT_FUNDS' || err.code === 'INSUFFICIENT_BALANCE');
14
+ }
15
+ // Hard account spend ceiling hit (distinct from an empty wallet). Always needs
16
+ // a human — raising/clearing the cap — never a retry or top-up.
17
+ function isSpendCapExceeded(err) {
18
+ return err instanceof client_1.MyApiError && err.code === 'SPEND_CAP_EXCEEDED';
19
+ }
20
+ // The auto_recharge state on an INSUFFICIENT_FUNDS error, if the backend set it.
21
+ function autoRechargeState(err) {
22
+ if (!(err instanceof client_1.MyApiError))
23
+ return undefined;
24
+ const s = err.body?.auto_recharge;
25
+ return typeof s === 'string' ? s : undefined;
26
+ }
27
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
28
+ // Wrap a billable call so an empty wallet with an auto-recharge IN FLIGHT is
29
+ // polled-and-retried instead of failing — the whole point of the 402 split,
30
+ // so an autonomous agent doesn't escalate to a human for a refill that's
31
+ // already coming. Retries ONLY on `INSUFFICIENT_FUNDS` + `auto_recharge ===
32
+ // 'in_flight'`, sleeping the server-provided `retry_after_seconds` (fallback
33
+ // 5s), up to maxRetries. Every other case — capped / no_pm / failed /
34
+ // disabled, or a `SPEND_CAP_EXCEEDED` hard ceiling — rethrows immediately:
35
+ // those need a human (top up, fix the card, raise the cap), not a retry.
36
+ async function withFundsRetry(call, opts = {}) {
37
+ const maxRetries = opts.maxRetries ?? 3;
38
+ const sleep = opts.sleep ?? defaultSleep;
39
+ let attempt = 0;
40
+ for (;;) {
41
+ try {
42
+ return await call();
43
+ }
44
+ catch (err) {
45
+ if (attempt >= maxRetries || !isInsufficientFunds(err) || autoRechargeState(err) !== 'in_flight') {
46
+ throw err;
47
+ }
48
+ attempt++;
49
+ const hinted = err.body?.retry_after_seconds;
50
+ const waitSec = typeof hinted === 'number' && hinted > 0 ? hinted : 5;
51
+ opts.onRetry?.(attempt, waitSec);
52
+ await sleep(waitSec * 1000);
53
+ }
54
+ }
55
+ }
package/dist/hq.d.ts CHANGED
@@ -138,6 +138,26 @@ export declare function setupPayment(apiKey: string): Promise<{
138
138
  export declare function topUp(apiKey: string, amountDollars: number): Promise<{
139
139
  new_balance_display: string;
140
140
  }>;
141
+ export type RechargeStatus = 'succeeded' | 'failed' | 'capped' | 'no_pm' | 'pending';
142
+ export interface AutoRechargeConfig {
143
+ enabled: boolean;
144
+ threshold_cents: number | null;
145
+ amount_cents: number | null;
146
+ monthly_cap_cents: number | null;
147
+ month_to_date_recharged_cents: number;
148
+ has_payment_method: boolean;
149
+ last_recharge_status: RechargeStatus | null;
150
+ last_recharge_attempt_at: string | null;
151
+ }
152
+ export declare function getAutoRecharge(apiKey: string): Promise<AutoRechargeConfig>;
153
+ export interface SetAutoRechargeInput {
154
+ enabled: boolean;
155
+ threshold_cents?: number;
156
+ amount_cents?: number;
157
+ monthly_cap_cents?: number;
158
+ }
159
+ export declare function setAutoRecharge(apiKey: string, input: SetAutoRechargeInput): Promise<AutoRechargeConfig>;
160
+ export declare function disableAutoRecharge(apiKey: string): Promise<void>;
141
161
  export type DoctorSeverity = 'ok' | 'warn' | 'crit';
142
162
  export interface DoctorEntityRef {
143
163
  slot: string;
@@ -152,11 +172,13 @@ export interface DoctorIssue {
152
172
  category?: string;
153
173
  message: string;
154
174
  hint?: string;
175
+ operator_only?: boolean;
155
176
  }
156
177
  export interface DoctorSection {
157
178
  name: string;
158
179
  summary: string;
159
180
  issues: DoctorIssue[];
181
+ resource_count?: number;
160
182
  }
161
183
  export interface DoctorReport {
162
184
  org_id: string;
package/dist/hq.js CHANGED
@@ -27,6 +27,9 @@ exports.getBillingHistory = getBillingHistory;
27
27
  exports.getBillingUsage = getBillingUsage;
28
28
  exports.setupPayment = setupPayment;
29
29
  exports.topUp = topUp;
30
+ exports.getAutoRecharge = getAutoRecharge;
31
+ exports.setAutoRecharge = setAutoRecharge;
32
+ exports.disableAutoRecharge = disableAutoRecharge;
30
33
  exports.getDoctor = getDoctor;
31
34
  const client_1 = require("./client");
32
35
  const config_1 = require("./config");
@@ -170,6 +173,21 @@ async function setupPayment(apiKey) {
170
173
  async function topUp(apiKey, amountDollars) {
171
174
  return (0, client_1.request)('POST', `${config_1.HQ_BASE}/hq/billing/topup`, apiKey, { amount_dollars: amountDollars });
172
175
  }
176
+ async function getAutoRecharge(apiKey) {
177
+ return (0, client_1.request)('GET', `${config_1.HQ_BASE}/hq/billing/auto-recharge`, apiKey);
178
+ }
179
+ // Enable/update auto-recharge. The backend validates invariants and rejects
180
+ // with a 400 (`MISSING_FIELDS`, `AMOUNT_BELOW_FLOOR` [$5 floor],
181
+ // `AMOUNT_LT_THRESHOLD`, `CAP_LT_AMOUNT`, `NO_PAYMENT_METHOD`) — surfaced as a
182
+ // MyApiError. Returns the full config on success.
183
+ async function setAutoRecharge(apiKey, input) {
184
+ return (0, client_1.request)('PUT', `${config_1.HQ_BASE}/hq/billing/auto-recharge`, apiKey, input);
185
+ }
186
+ // Disable auto-recharge (the threshold/amount/cap are preserved for easy
187
+ // re-enable). Backend returns 204 No Content.
188
+ async function disableAutoRecharge(apiKey) {
189
+ return (0, client_1.request)('DELETE', `${config_1.HQ_BASE}/hq/billing/auto-recharge`, apiKey);
190
+ }
173
191
  async function getDoctor(apiKey, orgId) {
174
192
  return (0, client_1.request)('GET', `${config_1.HQ_BASE}/hq/orgs/${encodeURIComponent(orgId)}/doctor`, apiKey);
175
193
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types';
2
2
  export * from './client';
3
+ export * from './funds';
3
4
  export * as hq from './hq';
4
5
  export * as auth from './auth';
5
6
  export * as domain from './domain';
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.task = exports.queue = exports.git = exports.container = exports.payments = exports.fn = exports.crm = exports.database = exports.llm = exports.audience = exports.company = exports.people = exports.url = exports.workflow = exports.webhook = exports.storage = exports.pixel = exports.image = exports.funnel = exports.email = exports.domain = exports.auth = exports.hq = void 0;
40
40
  __exportStar(require("./types"), exports);
41
41
  __exportStar(require("./client"), exports);
42
+ __exportStar(require("./funds"), exports);
42
43
  // Note: config constants (STORAGE_BASE etc.) are NOT re-exported from the
43
44
  // barrel. TypeScript compiles `export * from './config'` to a runtime
44
45
  // `__exportStar` call that Node's cjs-module-lexer can't see through, so
package/dist/services.js CHANGED
@@ -48,8 +48,8 @@ exports.SERVICES = [
48
48
  },
49
49
  // ── send ────────────────────────────────────────────────────────────
50
50
  {
51
- // Customer-facing email surface — mailboxes, campaigns, templates,
52
- // warmup. Launched 2026-05-17: the backend lifted the pre-launch gate
51
+ // Customer-facing email surface — mailboxes, transactional send,
52
+ // templates, warmup. Launched 2026-05-17: the backend lifted the pre-launch gate
53
53
  // (the [disabled, pre-launch] / 503 SERVICE_NOT_LAUNCHED state is gone).
54
54
  // Sibling `my-email-verify-api` is the sync single-address verifier.
55
55
  module: 'email',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/sdk",
3
3
  "license": "Apache-2.0",
4
- "version": "2.0.1",
4
+ "version": "2.2.0",
5
5
  "description": "TypeScript SDK for the MyAPI ecosystem",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
package/src/auth.ts CHANGED
@@ -20,9 +20,12 @@ export const EXPOSES: Exposes = [
20
20
  'GET /auth/orgs/{org_id}/tenant',
21
21
  'POST /auth/orgs/{org_id}/clients',
22
22
  'GET /auth/orgs/{org_id}/clients',
23
+ 'DELETE /auth/orgs/{org_id}/clients/{client_id}',
24
+ 'POST /auth/orgs/{org_id}/clients/{client_id}/rotate',
23
25
  'GET /auth/orgs/{org_id}/usage',
24
26
  'POST /auth/orgs/{org_id}/domain',
25
27
  'GET /auth/orgs/{org_id}/domain',
28
+ 'POST /auth/orgs/{org_id}/domain/verify',
26
29
  'DELETE /auth/orgs/{org_id}/domain',
27
30
  ];
28
31
 
@@ -70,13 +73,21 @@ export interface AuthUsage {
70
73
  period: string; // 'YYYY-MM'
71
74
  active_users: number;
72
75
  price_cents_each: number;
76
+ as_of?: string; // RFC3339 freshness marker — MAU is aggregated, not real-time
73
77
  }
74
78
 
75
- // A custom auth domain (e.g. auth.acme.com) for the tenant's hosted login + issuer.
79
+ // A custom auth domain (e.g. auth.acme.com) for the tenant's hosted login +
80
+ // issuer. Lifecycle: awaiting_verification (publish the TXT challenge, then
81
+ // `verify`) → pending (publish the A record, TLS provisions) → active.
76
82
  export interface AuthDomain {
77
83
  domain: string;
78
- status: 'pending' | 'active';
79
- dns: { type: string; name: string; value: string };
84
+ status: 'awaiting_verification' | 'pending' | 'active';
85
+ // Present while `awaiting_verification` the ownership-challenge TXT record
86
+ // to publish, then call verifyDomain().
87
+ verification?: { type: string; name: string; value: string };
88
+ // Present once `pending`/`active` — the A record to publish (withheld until
89
+ // ownership is verified).
90
+ dns?: { type: string; name: string; value: string };
80
91
  next?: string;
81
92
  issuer?: string; // present once active
82
93
  login_url?: string; // present once active
@@ -107,6 +118,19 @@ export async function listClients(apiKey: string, orgId: string): Promise<ListCl
107
118
  return request('GET', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/clients`, apiKey);
108
119
  }
109
120
 
121
+ // Delete (revoke) an OIDC client. Irreversible — the client_id stops
122
+ // authenticating immediately.
123
+ export async function deleteClient(apiKey: string, orgId: string, clientId: string): Promise<{ client_id: string; deleted: boolean }> {
124
+ return request('DELETE', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/clients/${encodeURIComponent(clientId)}`, apiKey);
125
+ }
126
+
127
+ // Rotate a confidential ('web') client's secret. Returns the NEW secret exactly
128
+ // once; the previous secret stops working immediately. (SPA clients have no
129
+ // secret — rotating one is rejected by the backend.)
130
+ export async function rotateClient(apiKey: string, orgId: string, clientId: string): Promise<AuthClient> {
131
+ return request('POST', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/clients/${encodeURIComponent(clientId)}/rotate`, apiKey);
132
+ }
133
+
110
134
  // Monthly-active-user usage for the current period (auth is billed per MAU).
111
135
  export async function getUsage(apiKey: string, orgId: string): Promise<AuthUsage> {
112
136
  return request('GET', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/usage`, apiKey);
@@ -124,6 +148,14 @@ export async function getDomain(apiKey: string, orgId: string): Promise<AuthDoma
124
148
  return request('GET', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/domain`, apiKey);
125
149
  }
126
150
 
151
+ // Verify ownership of the org's custom auth domain by checking the published
152
+ // TXT challenge. On success the domain advances awaiting_verification → pending
153
+ // (and the response carries the A record). Rejects with 422 DOMAIN_NOT_VERIFIED
154
+ // if the TXT record isn't found yet. Idempotent once already pending/active.
155
+ export async function verifyDomain(apiKey: string, orgId: string): Promise<AuthDomain> {
156
+ return request('POST', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/domain/verify`, apiKey);
157
+ }
158
+
127
159
  export async function deleteDomain(apiKey: string, orgId: string, domain: string): Promise<{ domain: string; status: string }> {
128
160
  return request('DELETE', `${BASE_URL}/auth/orgs/${encodeURIComponent(orgId)}/domain`, apiKey, { domain });
129
161
  }
package/src/funds.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { MyApiError } from './client';
2
+
3
+ // The auto-recharge state the backend carries on an `INSUFFICIENT_FUNDS` 402.
4
+ // `in_flight` = a refill was triggered and is on its way (retry); everything
5
+ // else needs a human action, not a retry.
6
+ export type AutoRechargeState = 'in_flight' | 'disabled' | 'capped' | 'no_pm' | 'failed';
7
+
8
+ // Wallet empty. Matches the unified `INSUFFICIENT_FUNDS` and the legacy
9
+ // `INSUFFICIENT_BALANCE` (still emitted by imagegen's free-tier path until the
10
+ // backend finishes the split — tolerate both).
11
+ export function isInsufficientFunds(err: unknown): err is MyApiError {
12
+ return err instanceof MyApiError &&
13
+ (err.code === 'INSUFFICIENT_FUNDS' || err.code === 'INSUFFICIENT_BALANCE');
14
+ }
15
+
16
+ // Hard account spend ceiling hit (distinct from an empty wallet). Always needs
17
+ // a human — raising/clearing the cap — never a retry or top-up.
18
+ export function isSpendCapExceeded(err: unknown): err is MyApiError {
19
+ return err instanceof MyApiError && err.code === 'SPEND_CAP_EXCEEDED';
20
+ }
21
+
22
+ // The auto_recharge state on an INSUFFICIENT_FUNDS error, if the backend set it.
23
+ export function autoRechargeState(err: unknown): AutoRechargeState | undefined {
24
+ if (!(err instanceof MyApiError)) return undefined;
25
+ const s = err.body?.auto_recharge;
26
+ return typeof s === 'string' ? (s as AutoRechargeState) : undefined;
27
+ }
28
+
29
+ export interface FundsRetryOptions {
30
+ // Max poll-and-retry attempts on an in-flight refill (default 3).
31
+ maxRetries?: number;
32
+ // Injectable sleep (tests pass a no-op; default is real setTimeout).
33
+ sleep?: (ms: number) => Promise<void>;
34
+ // Called before each wait — e.g. to log "balance low, refill in flight…".
35
+ onRetry?: (attempt: number, waitSeconds: number) => void;
36
+ }
37
+
38
+ const defaultSleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
39
+
40
+ // Wrap a billable call so an empty wallet with an auto-recharge IN FLIGHT is
41
+ // polled-and-retried instead of failing — the whole point of the 402 split,
42
+ // so an autonomous agent doesn't escalate to a human for a refill that's
43
+ // already coming. Retries ONLY on `INSUFFICIENT_FUNDS` + `auto_recharge ===
44
+ // 'in_flight'`, sleeping the server-provided `retry_after_seconds` (fallback
45
+ // 5s), up to maxRetries. Every other case — capped / no_pm / failed /
46
+ // disabled, or a `SPEND_CAP_EXCEEDED` hard ceiling — rethrows immediately:
47
+ // those need a human (top up, fix the card, raise the cap), not a retry.
48
+ export async function withFundsRetry<T>(call: () => Promise<T>, opts: FundsRetryOptions = {}): Promise<T> {
49
+ const maxRetries = opts.maxRetries ?? 3;
50
+ const sleep = opts.sleep ?? defaultSleep;
51
+ let attempt = 0;
52
+ for (;;) {
53
+ try {
54
+ return await call();
55
+ } catch (err) {
56
+ if (attempt >= maxRetries || !isInsufficientFunds(err) || autoRechargeState(err) !== 'in_flight') {
57
+ throw err;
58
+ }
59
+ attempt++;
60
+ const hinted = (err as MyApiError).body?.retry_after_seconds;
61
+ const waitSec = typeof hinted === 'number' && hinted > 0 ? hinted : 5;
62
+ opts.onRetry?.(attempt, waitSec);
63
+ await sleep(waitSec * 1000);
64
+ }
65
+ }
66
+ }
package/src/hq.ts CHANGED
@@ -281,6 +281,51 @@ export async function topUp(apiKey: string, amountDollars: number): Promise<{ ne
281
281
  return request('POST', `${BASE_URL}/hq/billing/topup`, apiKey, { amount_dollars: amountDollars });
282
282
  }
283
283
 
284
+ // ── Auto-recharge ──────────────────────────────────────────────────────
285
+ // Keeps the prepaid wallet funded without a human in the loop: when the
286
+ // balance drops below `threshold_cents`, the backend charges the saved card
287
+ // `amount_cents` off-session, bounded by `monthly_cap_cents`. Opt-in, off by
288
+ // default; enabling requires a saved payment method.
289
+
290
+ export type RechargeStatus = 'succeeded' | 'failed' | 'capped' | 'no_pm' | 'pending';
291
+
292
+ export interface AutoRechargeConfig {
293
+ enabled: boolean;
294
+ threshold_cents: number | null;
295
+ amount_cents: number | null;
296
+ monthly_cap_cents: number | null;
297
+ // Auto-recharge dollars charged this calendar month (vs the cap).
298
+ month_to_date_recharged_cents: number;
299
+ has_payment_method: boolean;
300
+ last_recharge_status: RechargeStatus | null;
301
+ last_recharge_attempt_at: string | null;
302
+ }
303
+
304
+ export async function getAutoRecharge(apiKey: string): Promise<AutoRechargeConfig> {
305
+ return request('GET', `${BASE_URL}/hq/billing/auto-recharge`, apiKey);
306
+ }
307
+
308
+ export interface SetAutoRechargeInput {
309
+ enabled: boolean;
310
+ threshold_cents?: number;
311
+ amount_cents?: number;
312
+ monthly_cap_cents?: number;
313
+ }
314
+
315
+ // Enable/update auto-recharge. The backend validates invariants and rejects
316
+ // with a 400 (`MISSING_FIELDS`, `AMOUNT_BELOW_FLOOR` [$5 floor],
317
+ // `AMOUNT_LT_THRESHOLD`, `CAP_LT_AMOUNT`, `NO_PAYMENT_METHOD`) — surfaced as a
318
+ // MyApiError. Returns the full config on success.
319
+ export async function setAutoRecharge(apiKey: string, input: SetAutoRechargeInput): Promise<AutoRechargeConfig> {
320
+ return request('PUT', `${BASE_URL}/hq/billing/auto-recharge`, apiKey, input);
321
+ }
322
+
323
+ // Disable auto-recharge (the threshold/amount/cap are preserved for easy
324
+ // re-enable). Backend returns 204 No Content.
325
+ export async function disableAutoRecharge(apiKey: string): Promise<void> {
326
+ return request('DELETE', `${BASE_URL}/hq/billing/auto-recharge`, apiKey);
327
+ }
328
+
284
329
  // ── Org doctor ─────────────────────────────────────────────────────────
285
330
  // Aggregated consistency report across slots. The endpoint resolves the
286
331
  // reported org from the API key's binding, so the `{org_id}` path param
@@ -310,12 +355,26 @@ export interface DoctorIssue {
310
355
  category?: string;
311
356
  message: string;
312
357
  hint?: string;
358
+ // True when the issue is platform-side and NOT actionable by the customer
359
+ // (e.g. a degraded internal dependency the MyAPI team owns). The backend
360
+ // sets this together with `category: 'internal'` and a customer-appropriate
361
+ // message; the operator detail is routed to an internal sink, not here.
362
+ // Consumers should surface these for transparency but must NOT count them
363
+ // as customer-actionable failures (they don't fail `doctor`'s exit code).
364
+ operator_only?: boolean;
313
365
  }
314
366
 
315
367
  export interface DoctorSection {
316
368
  name: string;
317
369
  summary: string;
318
370
  issues: DoctorIssue[];
371
+ // Authoritative count of resources of this section's kind in the org,
372
+ // independent of how many issues were emitted. Lets a consumer tell
373
+ // "zero resources" apart from "resources present, all healthy" without
374
+ // inferring it from `issues.length` (which breaks the moment the backend
375
+ // stops emitting an `ok` row per healthy resource). Optional: older
376
+ // backends omit it, and consumers must fall back to the issue-count proxy.
377
+ resource_count?: number;
319
378
  }
320
379
 
321
380
  export interface DoctorReport {
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types';
2
2
  export * from './client';
3
+ export * from './funds';
3
4
  // Note: config constants (STORAGE_BASE etc.) are NOT re-exported from the
4
5
  // barrel. TypeScript compiles `export * from './config'` to a runtime
5
6
  // `__exportStar` call that Node's cjs-module-lexer can't see through, so
package/src/services.ts CHANGED
@@ -75,8 +75,8 @@ export const SERVICES: readonly ServiceMeta[] = [
75
75
 
76
76
  // ── send ────────────────────────────────────────────────────────────
77
77
  {
78
- // Customer-facing email surface — mailboxes, campaigns, templates,
79
- // warmup. Launched 2026-05-17: the backend lifted the pre-launch gate
78
+ // Customer-facing email surface — mailboxes, transactional send,
79
+ // templates, warmup. Launched 2026-05-17: the backend lifted the pre-launch gate
80
80
  // (the [disabled, pre-launch] / 503 SERVICE_NOT_LAUNCHED state is gone).
81
81
  // Sibling `my-email-verify-api` is the sync single-address verifier.
82
82
  module: 'email',