import { ZodType } from 'zod'; interface RateLimiter { /** * Acquire 1 token. Resolves when granted (immediately if burst capacity * is available, otherwise after a delay). * * @param scope Independently-throttled bucket key (typically `seller:${id}`). */ acquire(scope: string): Promise; } interface TokenBucketOptions { /** Tokens per second refill. Default 24 (≈1440 req/min). */ refillPerSecond?: number; /** Max burst (bucket capacity). Default 60. */ burst?: number; /** Override "now" for tests. Returns ms. */ now?: () => number; /** Evict buckets idle for more than this many ms. Default 600_000 (10 min). * Set to 0 to disable GC (useful in tests). */ idleEvictMs?: number; } declare class TokenBucketRateLimiter implements RateLimiter { private readonly buckets; private readonly refillPerSecond; private readonly burst; private readonly now; private readonly idleEvictMs; /** Counter — every N acquires we sweep idle buckets. */ private acquireSinceLastSweep; private static readonly SWEEP_EVERY_N_ACQUIRES; constructor(options?: TokenBucketOptions); acquire(scope: string): Promise; /** Inspect tokens (for tests/diagnostics). */ inspect(scope: string): { tokens: number; }; /** Number of currently-tracked buckets. Diagnostics only. */ bucketCount(): number; /** Force-evict buckets that have been idle longer than `idleEvictMs`. * We don't bother checking token count — recreating an idle bucket * later is essentially free (one Map.set + one allocation), and any * bucket that's been idle for >10 min has long since refilled to burst * anyway. */ sweepIdleBuckets(now?: number): number; private maybeSweep; private refill; } /** No-op limiter for tests / when you want to bypass throttling entirely. */ declare class NoopRateLimiter implements RateLimiter { acquire(): Promise; } interface RetryOptions { /** Max attempts (including the first). Default 4. */ maxAttempts?: number; /** Base delay in ms before the first retry. Default 200. */ baseDelayMs?: number; /** Max delay between retries in ms. Default 8000. */ maxDelayMs?: number; /** Jitter factor 0..1. Default 0.3 (±30%). */ jitter?: number; /** Custom callback fired before each retry. */ onRetry?: (attempt: number, lastError: unknown) => void; } interface RetryDecision { shouldRetry: boolean; /** Override delay (e.g. from Retry-After header). */ delayMsOverride?: number; } interface RetryContext { /** HTTP method of the request being attempted, uppercase. Default "GET". */ method?: string; /** Attempt number (1-based). */ attempt?: number; } /** A function that decides whether a thrown error / response is retryable. */ type RetryClassifier = (error: unknown, response: Response | null, ctx?: RetryContext) => RetryDecision; /** * Default classifier — retry on 5xx, 429, network errors. By default, * **only retries idempotent methods** (GET/HEAD/OPTIONS/PUT/DELETE). POST * and PATCH are NOT retried because MELI's gateway can persist a request * after a 5xx (split-brain), which would create duplicate listings, * double-answers, or duplicate promo opt-ins on retry. * * Callers who know their POST endpoint is idempotent (or who supply an * `X-Idempotency-Key` header) can override via `retryClassifier`. */ declare const defaultRetryClassifier: RetryClassifier; /** * Run an async operation with exponential backoff. The operation receives * the current attempt number (1-based) and is expected to either resolve * with a value or throw. The classifier decides whether to retry. * * For HTTP, prefer the `fetchWithRetry` helper instead — it composes this * with response inspection. */ declare function withRetry(op: (attempt: number) => Promise, classifier?: RetryClassifier, options?: RetryOptions, ctx?: RetryContext): Promise; /** * Variant of `withRetry` specialized for `fetch`. The op MUST return the * `Response`, NOT throw on 4xx/5xx — this helper inspects the response * status itself. Network errors (fetch throwing) are retried per the * classifier. */ declare function fetchWithRetry(url: string, init: RequestInit, options?: RetryOptions, classifier?: RetryClassifier, fetchImpl?: typeof fetch): Promise; declare function sleep(ms: number): Promise; declare const AUTHORIZATION_URL_BY_SITE: Record; interface OAuthAppCredentials { clientId: string; clientSecret: string; /** Must be registered on the MELI app dashboard. */ redirectUri: string; } interface MeliOAuthTokens { access_token: string; refresh_token: string; /** Seconds-from-now until access_token expires. Typically 21600 (6h). */ expires_in: number; /** Unix-seconds wall-clock at which access_token will expire. */ access_token_expires_at: number; /** MELI-side user id of the authorized seller. */ user_id: number; scope: string; token_type: "bearer"; } /** * Pluggable token store. Production deployments back this with Redis / * Postgres / Vercel KV. The `InMemoryOAuthStore` below is suitable for * tests + single-process demos. */ interface OAuthTokenStore { /** Read tokens for a specific seller (by MELI user id). */ read(userId: number): Promise; /** Atomically replace tokens. Implementations MUST handle concurrent writes. */ write(userId: number, tokens: MeliOAuthTokens): Promise; /** Remove tokens (e.g. when seller revokes). */ remove?(userId: number): Promise; } interface BuildAuthUrlInput { app: OAuthAppCredentials; /** MELI site identifier — controls which auth host is used. */ site: keyof typeof AUTHORIZATION_URL_BY_SITE; /** State for CSRF protection (random nonce, validated on callback). */ state: string; /** Override scopes. Default: `offline_access read write`. */ scopes?: string[]; } declare function buildAuthorizationUrl(input: BuildAuthUrlInput): string; declare function exchangeAuthorizationCode(app: OAuthAppCredentials, code: string, fetchImpl?: typeof fetch): Promise; declare function refreshTokens(app: OAuthAppCredentials, refreshToken: string, fetchImpl?: typeof fetch): Promise; /** * Returns a valid access_token for the given seller, refreshing if needed. * Coalesces concurrent calls so only one HTTP refresh fires per seller per * window. Atomically updates the store with the rotated refresh_token. * * Throws `MeliAuthError` if no tokens are stored OR if the refresh fails * permanently (e.g. revoked, expired beyond 4 months). */ declare function ensureAccessToken(args: { userId: number; app: OAuthAppCredentials; store: OAuthTokenStore; /** Refresh ahead of expiry by this many seconds. Default 60. */ preflightWindowSeconds?: number; /** Override "now" for tests. Returns Unix seconds. */ now?: () => number; /** Override fetch (mocked in tests / Edge runtime). Default global fetch. */ fetchImpl?: typeof fetch; }): Promise; declare class InMemoryOAuthStore implements OAuthTokenStore { private state; read(userId: number): Promise; write(userId: number, tokens: MeliOAuthTokens): Promise; remove(userId: number): Promise; } interface TelemetryRequestEvent { /** Stable id for correlating onRequest ↔ onResponse / onRetry. */ requestId: string; /** Wall-clock start in ms (Date.now). */ startedAt: number; method: string; /** Full URL including query string. */ url: string; /** Path without query string for low-cardinality span names. */ path: string; /** Attempt number (1-based). */ attempt: number; } interface TelemetryResponseEvent { requestId: string; /** Wall-clock end in ms. */ endedAt: number; /** Total duration in ms (endedAt - startedAt). */ durationMs: number; status: number; /** Number of attempts that ran before this response (1 = no retries). */ attempts: number; /** MELI's request id from the response header, if present. */ meliRequestId?: string; } interface TelemetryRetryEvent { requestId: string; attempt: number; /** Reason: 5xx status, 429, network error, etc. */ reason: "status" | "network" | "timeout"; status?: number; /** Backoff delay before next attempt, in ms. */ delayMs: number; } interface TelemetryRateLimitEvent { /** Bucket scope (`seller:`, `bearer:`, or "anon"). */ scope: string; /** How long we waited for a token, in ms. */ waitMs: number; } interface TelemetryHooks { /** Fired before the HTTP call leaves the client. */ onRequest?: (event: TelemetryRequestEvent) => void; /** Fired after the response is received (success or non-retryable error). */ onResponse?: (event: TelemetryResponseEvent) => void; /** Fired before each retry attempt. */ onRetry?: (event: TelemetryRetryEvent) => void; /** Fired when a rate-limit token wait completes. Only emitted when wait > 0. */ onRateLimitWait?: (event: TelemetryRateLimitEvent) => void; } /** * No-op telemetry. Used as the default when the host doesn't configure hooks. * Defined here (instead of `?? {}` inline) so the JIT can devirtualize the * empty path. */ declare const noopTelemetry: Required; /** * Generate a request id without depending on `crypto.randomUUID()`. Edge * runtimes have it, but Node 18- on Lambda doesn't. */ declare function generateRequestId(): string; type AuthMode = { kind: "bearer"; accessToken: string; } | { kind: "oauth"; userId: number; app: OAuthAppCredentials; store: OAuthTokenStore; } | { kind: "none"; }; interface MeliClientOptions { /** Auth strategy. Use `bearer` for direct tokens, `oauth` for managed * refresh, or `none` for public endpoints (search, sites, currencies). */ auth: AuthMode; /** Override base URL — useful for testing against a mock server. */ baseUrl?: string; /** Override fetch (e.g. msw). Defaults to globalThis.fetch. */ fetch?: typeof fetch; /** Rate limiter implementation. Default: TokenBucketRateLimiter. */ rateLimiter?: RateLimiter; /** Retry options forwarded to `fetchWithRetry`. */ retry?: RetryOptions; /** Custom retry classifier. */ retryClassifier?: RetryClassifier; /** User-Agent string. Default: `@ar-agents/mercadolibre/`. */ userAgent?: string; /** When true, skip Zod validation on responses (hot paths). Default false. */ skipResponseValidation?: boolean; /** Pluggable telemetry hooks (OpenTelemetry, Sentry, Datadog, custom). */ telemetry?: TelemetryHooks; /** Per-request timeout in milliseconds. Default 30000. * Wedged TCP connections that never return data otherwise burn a Vercel * Edge function's entire 25-60s budget on attempt #1 and never retry. */ requestTimeoutMs?: number; } interface FetchOptions { method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; /** Path relative to base URL (must start with `/`). */ path: string; /** Query string parameters. */ query?: Record; /** Pre-serialized body. If set, body is sent as-is. */ body?: unknown; /** When set, response is parsed against this Zod schema. */ responseSchema?: ZodType; /** Override per-call User-Agent. */ userAgent?: string; /** Per-call retry override. */ retry?: RetryOptions; /** AbortSignal. */ signal?: AbortSignal; /** Override the rate-limit scope. Default: derived from auth (`seller:`). */ rateLimitScope?: string; } declare class MeliClient { readonly baseUrl: string; private readonly auth; private readonly fetchImpl; private readonly rateLimiter; private readonly retry; private readonly retryClassifier; private readonly userAgent; private readonly skipResponseValidation; private readonly telemetry; private readonly requestTimeoutMs; constructor(options: MeliClientOptions); /** * Like `fetch`, but returns the raw `Response`. Used for binary endpoints * (e.g., `/shipment_labels` returning PDF/ZPL). Goes through the same * auth + rate-limit + retry + telemetry plumbing. * * Throws on non-2xx the same way `fetch` does. */ fetchRaw(options: FetchOptions & { acceptHeader?: string; }): Promise; /** * Make a request. Returns the parsed-and-validated response. * * Throws: * - `MeliAuthError` if OAuth refresh fails * - `MeliApiError` if status >= 400 after retries * - `MeliNetworkError` if fetch threw * - `MeliValidationError` if response failed Zod validation */ fetch(options: FetchOptions): Promise; /** Internal — runs the full request pipeline. JSON-parses unless told otherwise. */ private executeRequest; private resolveAuthHeader; private deriveRateLimitScope; private buildUrl; } export { type AuthMode as A, type BuildAuthUrlInput as B, type FetchOptions as F, InMemoryOAuthStore as I, MeliClient as M, NoopRateLimiter as N, type OAuthAppCredentials as O, type RateLimiter as R, type TelemetryHooks as T, type MeliClientOptions as a, type MeliOAuthTokens as b, type OAuthTokenStore as c, type RetryClassifier as d, type RetryContext as e, type RetryDecision as f, type RetryOptions as g, type TelemetryRateLimitEvent as h, type TelemetryRequestEvent as i, type TelemetryResponseEvent as j, type TelemetryRetryEvent as k, type TokenBucketOptions as l, TokenBucketRateLimiter as m, buildAuthorizationUrl as n, defaultRetryClassifier as o, ensureAccessToken as p, exchangeAuthorizationCode as q, fetchWithRetry as r, generateRequestId as s, noopTelemetry as t, refreshTokens as u, sleep as v, withRetry as w };