@myapihq/sdk 2.4.0 → 2.4.2

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/src/email.ts DELETED
@@ -1,247 +0,0 @@
1
- import { request } from './client';
2
- import { EMAIL_BASE as BASE_URL } from './config';
3
- import type { Exposes } from './exposes';
4
-
5
- export const EXPOSES: Exposes = [
6
- // Mailbox / sending activation
7
- 'POST /email/mailboxes/create',
8
- 'GET /email/mailboxes',
9
- 'DELETE /email/mailboxes/{address}',
10
- 'PUT /email/mailboxes/{address}/forwarding',
11
- 'DELETE /email/mailboxes/{address}/forwarding',
12
- 'POST /email/sending/activate',
13
- 'POST /email/orgs/{org_id}/domains/{domain}/mail-server-resync',
14
- // Send + read
15
- 'POST /email/send',
16
- 'GET /email/status/{message_id}',
17
- 'GET /email/sent',
18
- 'GET /email/inbox/{address}',
19
- 'GET /email/outbox/{address}',
20
- 'GET /email/message/{message_id}',
21
- // Warmup
22
- 'POST /email/warmup/start',
23
- 'POST /email/warmup/pause',
24
- 'POST /email/warmup/resume',
25
- 'POST /email/warmup/stop',
26
- 'GET /email/warmup/stats/{address}',
27
- // Templates
28
- 'POST /email/orgs/{org_id}/templates/generate',
29
- 'GET /email/orgs/{org_id}/template-jobs/{job_id}',
30
- 'GET /email/orgs/{org_id}/templates',
31
- 'GET /email/orgs/{org_id}/templates/{template_id}',
32
- 'POST /email/orgs/{org_id}/templates/{template_id}/edit',
33
- 'POST /email/orgs/{org_id}/templates/{template_id}/send-test',
34
- 'DELETE /email/orgs/{org_id}/templates/{template_id}',
35
- 'GET /email/templates/{template_id}/preview',
36
- // Verify
37
- 'POST /email/orgs/{org_id}/verify',
38
- 'POST /email/orgs/{org_id}/verify-bulk',
39
- 'GET /email/orgs/{org_id}/verify-jobs/{id}',
40
- ];
41
-
42
- export interface EmailMessage { message_id: string; from: string; subject: string; body?: string; received_at: string }
43
- export interface EmailTemplate { id: string; name: string; subject: string; preview_url: string; created_at: string; updated_at: string }
44
-
45
- // ── Email verification ──────────────────────────────────────────────────────
46
- //
47
- // Synchronous single-address verification — syntax + DNS + a Microsoft
48
- // GetCredentialType probe. Cheap layer only: ~50% of inputs return a
49
- // definitive verdict in <1s; the rest get verdict='unknown' with
50
- // smtp_recommended=true, suggesting a downstream SMTP probe (not in this
51
- // API today).
52
-
53
- export type VerifyVerdict = 'deliverable' | 'undeliverable' | 'unknown';
54
-
55
- export interface VerifyChecks {
56
- syntax: { valid: boolean; detail?: string };
57
- dns?: { valid: boolean; mx_records?: string[] };
58
- microsoft?: {
59
- verdict: string; // e.g. 'federated_unknown', 'not_microsoft_domain'
60
- if_exists_result: number; // Microsoft GetCredentialType enum
61
- domain_type: number;
62
- federated: boolean;
63
- };
64
- }
65
-
66
- export interface VerifyResult {
67
- email: string;
68
- verdict: VerifyVerdict;
69
- confidence: number; // 0..1
70
- smtp_recommended: boolean;
71
- checks: VerifyChecks;
72
- elapsed_ms: number;
73
- }
74
-
75
- export async function verifyEmail(apiKey: string, orgId: string, email: string): Promise<VerifyResult> {
76
- return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/verify`, apiKey, { email });
77
- }
78
-
79
- // One address's outcome in a bulk-verification job.
80
- export interface BulkVerifyResult {
81
- email: string;
82
- verdict: string; // deliverable | undeliverable | risky | unknown
83
- confidence: number;
84
- source: string; // phase1 | strategist | smtp
85
- classification?: string;
86
- mx?: string;
87
- rcpt_code?: number;
88
- catch_all?: boolean;
89
- detail?: string;
90
- }
91
-
92
- export interface VerifyJob {
93
- job_id: string;
94
- status: string; // pending | running | done | failed
95
- total: number;
96
- completed?: number;
97
- catch_all?: boolean;
98
- results?: BulkVerifyResult[];
99
- error?: string;
100
- }
101
-
102
- // verifyBulk kicks off an asynchronous bulk verification (1-500 addresses)
103
- // and returns immediately with a job_id. `catchAll` toggles the catch-all
104
- // check on SMTP-probed addresses (default true). Poll getVerifyJob for
105
- // status + results.
106
- export async function verifyBulk(apiKey: string, orgId: string, emails: string[], catchAll?: boolean): Promise<VerifyJob> {
107
- const body: { emails: string[]; catch_all?: boolean } = { emails };
108
- if (catchAll !== undefined) body.catch_all = catchAll;
109
- return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/verify-bulk`, apiKey, body);
110
- }
111
-
112
- export async function getVerifyJob(apiKey: string, orgId: string, jobId: string): Promise<VerifyJob> {
113
- return request('GET', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/verify-jobs/${encodeURIComponent(jobId)}`, apiKey);
114
- }
115
-
116
- // ── Mailbox ops (account-scoped) ─────────────────────────────────────────────
117
-
118
- export async function createMailbox(apiKey: string, domain: string, username: string, displayName?: string): Promise<{ address: string; created_at: string }> {
119
- return request('POST', `${BASE_URL}/email/mailboxes/create`, apiKey, { domain, username, display_name: displayName });
120
- }
121
-
122
- // Backend (2026-06-06): idempotent — 204 whether the mailbox existed or not.
123
- // May return 409 MAILBOX_IN_USE if the address is still referenced server-side.
124
- export async function deleteMailbox(apiKey: string, address: string): Promise<void> {
125
- return request('DELETE', `${BASE_URL}/email/mailboxes/${encodeURIComponent(address)}`, apiKey);
126
- }
127
-
128
- // Backend (2026-06-06): re-run Stalwart provisioning for a domain. Use when
129
- // mailbox-create on a domain returns DOMAIN_NOT_MAIL_READY /
130
- // MAILBOX_PROVISION_FAILED, or when the inbox endpoint returns
131
- // "mail server error" on a domain whose `email_infra_ready` flag is stale.
132
- // Idempotent.
133
- export async function mailServerResync(apiKey: string, orgId: string, domain: string): Promise<{ ok: boolean } & Record<string, unknown>> {
134
- return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/domains/${encodeURIComponent(domain)}/mail-server-resync`, apiKey);
135
- }
136
-
137
- export async function listMailboxes(apiKey: string, options?: { domain?: string; filter?: 'unassigned' }): Promise<{ address: string; created_at: string }[]> {
138
- const query = new URLSearchParams();
139
- if (options?.domain) query.append('domain', options.domain);
140
- if (options?.filter) query.append('filter', options.filter);
141
- const qStr = query.toString();
142
- return request('GET', `${BASE_URL}/email/mailboxes${qStr ? '?' + qStr : ''}`, apiKey);
143
- }
144
-
145
- export async function activateSending(apiKey: string, address: string): Promise<{
146
- address: string;
147
- sending_enabled: boolean;
148
- emails_quota_remaining: number;
149
- }> {
150
- return request('POST', `${BASE_URL}/email/sending/activate`, apiKey, { address });
151
- }
152
-
153
- // setForwarding redirects a copy of every incoming message to an external
154
- // address (server-side; the original is kept in the mailbox). `forwardTo`
155
- // must be a valid address and cannot equal the mailbox itself.
156
- export async function setForwarding(apiKey: string, address: string, forwardTo: string): Promise<{ address: string; forward_to: string }> {
157
- return request('PUT', `${BASE_URL}/email/mailboxes/${encodeURIComponent(address)}/forwarding`, apiKey, { forward_to: forwardTo });
158
- }
159
-
160
- // deleteForwarding stops forwarding for a mailbox.
161
- export async function deleteForwarding(apiKey: string, address: string): Promise<void> {
162
- return request('DELETE', `${BASE_URL}/email/mailboxes/${encodeURIComponent(address)}/forwarding`, apiKey);
163
- }
164
-
165
- // ── Sending and reading (account-scoped) ─────────────────────────────────────
166
-
167
- export async function sendEmail(apiKey: string, payload: { from: string; to: string[]; subject: string; html?: string; text?: string; template_id?: string; template_vars?: Record<string, string> }): Promise<{ message_id: string }> {
168
- return request('POST', `${BASE_URL}/email/send`, apiKey, payload);
169
- }
170
-
171
- export async function getEmailStatus(apiKey: string, messageId: string): Promise<{ status: string; delivered_at?: string }> {
172
- return request('GET', `${BASE_URL}/email/status/${encodeURIComponent(messageId)}`, apiKey);
173
- }
174
-
175
- export async function getSentEmails(apiKey: string, limit: number = 50, offset: number = 0): Promise<EmailMessage[]> {
176
- return request('GET', `${BASE_URL}/email/sent?limit=${limit}&offset=${offset}`, apiKey);
177
- }
178
-
179
- export async function getInbox(apiKey: string, address: string): Promise<EmailMessage[]> {
180
- return request('GET', `${BASE_URL}/email/inbox/${encodeURIComponent(address)}`, apiKey);
181
- }
182
-
183
- export async function getOutbox(apiKey: string, address: string): Promise<EmailMessage[]> {
184
- return request('GET', `${BASE_URL}/email/outbox/${encodeURIComponent(address)}`, apiKey);
185
- }
186
-
187
- export async function getMessage(apiKey: string, messageId: string, address: string): Promise<EmailMessage> {
188
- return request('GET', `${BASE_URL}/email/message/${encodeURIComponent(messageId)}?address=${encodeURIComponent(address)}`, apiKey);
189
- }
190
-
191
- // ── Warmup (account-scoped) ──────────────────────────────────────────────────
192
-
193
- export async function startWarmup(apiKey: string, address: string): Promise<{ warmup_status: string }> {
194
- return request('POST', `${BASE_URL}/email/warmup/start`, apiKey, { address });
195
- }
196
-
197
- export async function pauseWarmup(apiKey: string, address: string): Promise<void> {
198
- return request('POST', `${BASE_URL}/email/warmup/pause`, apiKey, { address });
199
- }
200
-
201
- export async function resumeWarmup(apiKey: string, address: string): Promise<void> {
202
- return request('POST', `${BASE_URL}/email/warmup/resume`, apiKey, { address });
203
- }
204
-
205
- export async function stopWarmup(apiKey: string, address: string): Promise<void> {
206
- return request('POST', `${BASE_URL}/email/warmup/stop`, apiKey, { address });
207
- }
208
-
209
- export async function getWarmupStats(apiKey: string, address: string): Promise<{ sent: number; landed_inbox: number; landed_spam: number; health_score: number }> {
210
- return request('GET', `${BASE_URL}/email/warmup/stats/${encodeURIComponent(address)}`, apiKey);
211
- }
212
-
213
- // ── Templates (org-scoped — brand context drives generation) ─────────────────
214
-
215
- export async function generateTemplate(apiKey: string, orgId: string, payload: { prompt: string; name: string; }): Promise<{ job_id: string; template_id: string; status: string; preview_url: string }> {
216
- return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/templates/generate`, apiKey, payload);
217
- }
218
-
219
- export async function getTemplateJobStatus(apiKey: string, orgId: string, jobId: string): Promise<{ status: 'processing' | 'completed' | 'failed'; template_id: string; result?: { subject: string; preview_url: string } }> {
220
- return request('GET', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/template-jobs/${encodeURIComponent(jobId)}`, apiKey);
221
- }
222
-
223
- export async function getTemplate(apiKey: string, orgId: string, templateId: string): Promise<EmailTemplate> {
224
- return request('GET', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/templates/${encodeURIComponent(templateId)}`, apiKey);
225
- }
226
-
227
- // Public preview — no auth required. The endpoint is unauthenticated per
228
- // the OpenAPI schema; access is gated by the unguessable template_id.
229
- export async function getTemplatePreview(templateId: string): Promise<{ html?: string; subject?: string; preview_url?: string } & Record<string, unknown>> {
230
- return request('GET', `${BASE_URL}/email/templates/${encodeURIComponent(templateId)}/preview`, undefined);
231
- }
232
-
233
- export async function editTemplate(apiKey: string, orgId: string, templateId: string, prompt: string): Promise<{ template_id: string; preview_url: string }> {
234
- return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/templates/${encodeURIComponent(templateId)}/edit`, apiKey, { prompt });
235
- }
236
-
237
- export async function sendTestEmail(apiKey: string, orgId: string, templateId: string, to: string): Promise<{ ok: boolean; message_id: string }> {
238
- return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/templates/${encodeURIComponent(templateId)}/send-test`, apiKey, { to });
239
- }
240
-
241
- export async function listTemplates(apiKey: string, orgId: string): Promise<EmailTemplate[]> {
242
- return request('GET', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/templates`, apiKey);
243
- }
244
-
245
- export async function deleteTemplate(apiKey: string, orgId: string, templateId: string): Promise<void> {
246
- return request('DELETE', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/templates/${encodeURIComponent(templateId)}`, apiKey);
247
- }
package/src/exposes.ts DELETED
@@ -1,16 +0,0 @@
1
- // OpenAPI-templated endpoint declaration for SDK service modules.
2
- //
3
- // Each service module (hq, domain, funnel, ...) exports an `EXPOSES: Exposes`
4
- // array listing the backend endpoints reachable through that module's
5
- // functions. The integrations coverage tool walks these to verify CLI ↔ SDK
6
- // parity (S-108).
7
- //
8
- // Form: `<METHOD> <PATH>` where path uses `{name}` for variable segments
9
- // matching OpenAPI 3.x style.
10
- //
11
- // Mirrors packages/cli/src/exposes.ts — kept duplicated to avoid runtime
12
- // dependencies between the packages (the SDK has zero runtime deps per NFR-005).
13
-
14
- export type Endpoint = `${'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'} /${string}`;
15
-
16
- export type Exposes = readonly Endpoint[];
package/src/function.ts DELETED
@@ -1,141 +0,0 @@
1
- import { request, MyApiError } from './client';
2
- import { ApiResponse } from './types';
3
- import { FUNCTION_BASE as BASE_URL } from './config';
4
- import type { Exposes } from './exposes';
5
-
6
- // Backend: Story 1 (metadata + scoped API key) and Story 2/4/5 (deploy,
7
- // runs, env) per myapi-hq/internal/routes/function/. The /logs endpoint is
8
- // still pending — do not add forward-compat fields the backend will ignore.
9
- export const EXPOSES: Exposes = [
10
- 'POST /function/orgs/{org_id}/functions',
11
- 'GET /function/orgs/{org_id}/functions',
12
- 'GET /function/orgs/{org_id}/functions/{id}',
13
- 'DELETE /function/orgs/{org_id}/functions/{id}',
14
- 'POST /function/orgs/{org_id}/functions/{id}/bundle',
15
- 'POST /function/orgs/{org_id}/functions/{id}/env',
16
- 'GET /function/orgs/{org_id}/functions/{id}/runs',
17
- ];
18
-
19
- export type TriggerType = 'http' | 'cron';
20
-
21
- // Mirrors the backend's `Function` struct in crud.go. `invocation_url`
22
- // stays an empty string until the function is deployed (uploadBundle).
23
- export interface Fn {
24
- id: string;
25
- org_id: string;
26
- name: string;
27
- trigger_type: TriggerType;
28
- cron_schedule?: string;
29
- invocation_url: string;
30
- created_at: string;
31
- updated_at: string;
32
- }
33
-
34
- export interface CreatePayload {
35
- name: string; // required; ^[a-z0-9][a-z0-9-]{0,49}$
36
- trigger_type?: TriggerType; // optional, defaults to 'http' server-side
37
- cron_schedule?: string; // required if trigger_type === 'cron'
38
- // Optional slot allow-list narrowing the minted function key's grants
39
- // (backend crud.go: CreateFunction `scopes`). Empty/omitted inherits the
40
- // deploying caller's grants (legacy). Grants can never exceed the caller.
41
- scopes?: string[];
42
- }
43
-
44
- // POST response envelope includes the scoped API key — returned ONCE. The
45
- // caller must persist it if they want to use it directly; the SDK does not
46
- // store it. The function's runtime shim (`globalThis.MYAPI`) uses this key
47
- // internally.
48
- export interface CreateResponse {
49
- function: Fn;
50
- scoped_api_key: string;
51
- scoped_api_key_id: string;
52
- }
53
-
54
- // uploadBundle response. The scoped API key is ROTATED on every deploy —
55
- // the value here is fresh and the only time it is knowable. The previous
56
- // scoped key is revoked server-side.
57
- export interface DeployResponse {
58
- function: Fn;
59
- invocation_url: string;
60
- scoped_api_key: string;
61
- }
62
-
63
- // One recorded invocation — mirrors the backend's `FunctionRun` struct.
64
- export interface FunctionRun {
65
- id: string;
66
- function_id: string;
67
- invoked_at: string;
68
- duration_ms?: number;
69
- status?: string;
70
- error_message?: string;
71
- }
72
-
73
- export async function createFunction(apiKey: string, orgId: string, payload: CreatePayload): Promise<CreateResponse> {
74
- return request('POST', `${BASE_URL}/function/orgs/${encodeURIComponent(orgId)}/functions`, apiKey, payload);
75
- }
76
-
77
- export async function listFunctions(apiKey: string, orgId: string): Promise<Fn[]> {
78
- return request('GET', `${BASE_URL}/function/orgs/${encodeURIComponent(orgId)}/functions`, apiKey);
79
- }
80
-
81
- export async function getFunction(apiKey: string, orgId: string, fnId: string): Promise<Fn> {
82
- return request('GET', `${BASE_URL}/function/orgs/${encodeURIComponent(orgId)}/functions/${encodeURIComponent(fnId)}`, apiKey);
83
- }
84
-
85
- export async function deleteFunction(apiKey: string, orgId: string, fnId: string): Promise<void> {
86
- return request('DELETE', `${BASE_URL}/function/orgs/${encodeURIComponent(orgId)}/functions/${encodeURIComponent(fnId)}`, apiKey);
87
- }
88
-
89
- // uploadBundle deploys a single-file JS bundle (Story 2). The backend
90
- // wraps it with the MYAPI shim and uploads to Cloudflare Workers. The
91
- // multipart field MUST be named `bundle`. Raw upload cap is 4MB.
92
- export async function uploadBundle(
93
- apiKey: string,
94
- orgId: string,
95
- fnId: string,
96
- bundle: Blob | Buffer | string,
97
- filename = 'bundle.js',
98
- ): Promise<DeployResponse> {
99
- const formData = new FormData();
100
- formData.append('bundle', new Blob([bundle as any], { type: 'application/javascript' }), filename);
101
-
102
- const response = await fetch(
103
- `${BASE_URL}/function/orgs/${encodeURIComponent(orgId)}/functions/${encodeURIComponent(fnId)}/bundle`,
104
- { method: 'POST', headers: { Authorization: `Bearer ${apiKey}` }, body: formData as any },
105
- );
106
-
107
- let result: any;
108
- try {
109
- result = await response.json();
110
- } catch {
111
- throw new MyApiError('invalid_json_response', response.status);
112
- }
113
- if (!response.ok || !result?.success) {
114
- const err = result?.error;
115
- const code = typeof err === 'object' ? (err?.code || 'unknown_error') : (err || 'unknown_error');
116
- const detail = typeof err === 'object' ? err?.message : undefined;
117
- throw new MyApiError(code, response.status, detail, typeof err === 'object' ? err : undefined);
118
- }
119
- return (result as ApiResponse<DeployResponse>).data as DeployResponse;
120
- }
121
-
122
- // setFunctionEnv writes a secret (Stripe key, etc.) as a Cloudflare Worker
123
- // Secret on the deployed script (Story 5). The function must already be
124
- // deployed. The value is never stored in MyAPI Postgres or returned.
125
- export async function setFunctionEnv(apiKey: string, orgId: string, fnId: string, name: string, value: string): Promise<void> {
126
- return request('POST', `${BASE_URL}/function/orgs/${encodeURIComponent(orgId)}/functions/${encodeURIComponent(fnId)}/env`, apiKey, { name, value });
127
- }
128
-
129
- // setFunctionEnvBulk writes multiple Worker Secrets in one call. The backend
130
- // (env.go) loops, calling Cloudflare once per secret, and returns `{set:N}`.
131
- // The single-secret `setFunctionEnv` above stays the canonical one-off path;
132
- // this is the bulk variant. The function must already be deployed.
133
- export async function setFunctionEnvBulk(apiKey: string, orgId: string, fnId: string, env: Record<string, string>): Promise<{ set: number }> {
134
- return request('POST', `${BASE_URL}/function/orgs/${encodeURIComponent(orgId)}/functions/${encodeURIComponent(fnId)}/env`, apiKey, { env });
135
- }
136
-
137
- // listFunctionRuns returns recent invocation records, most recent first
138
- // (Story 4). Capped at 100 server-side.
139
- export async function listFunctionRuns(apiKey: string, orgId: string, fnId: string): Promise<FunctionRun[]> {
140
- return request('GET', `${BASE_URL}/function/orgs/${encodeURIComponent(orgId)}/functions/${encodeURIComponent(fnId)}/runs`, apiKey);
141
- }
package/src/funds.ts DELETED
@@ -1,66 +0,0 @@
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
- }