/** * HTTP client wrapping the Frihet ERP REST API. * * Handles authentication, pagination, rate-limit retries, and error mapping. * * Pagination convention: every `list*`/`search*` method below keeps `after` * as its caller-facing param name (tools/*.ts and other callers pass * `{ after }` unchanged — it's a naming alias only), but the wire query * built for `requestPaginated` always sends it under the `cursor` key * (`cursor: params?.after`) — the query param the backend actually reads * (`req.query.cursor`, functions/src/publicApi.ts in Frihet-ERP). `after` * is never sent on the wire. See src/__tests__/pagination-cursor-param.test.ts. */ import type { CreateWebhookInput, CreateWebhookResult, PaginatedResponse, UpdateWebhookInput, Webhook } from "./types.js"; /** * Hard size caps for document responses. Enforced TWICE: precheck on * `Content-Length` (so an honest server doesn't waste bandwidth) and after * streaming (so a missing/lying `Content-Length` still can't trigger an * unbounded allocation). * * - PDF: 25 MiB — generous for any ERP-issued invoice PDF, including * embedded logos, Facturae XML attachments, and stamp signatures. * - XML: 5 MiB — UBL / Facturae / PEPPOL documents stay well under 1 MiB * in practice; 5 MiB absorbs any historical / annex-laden outlier. * * Anything larger is rejected with `413 payload_too_large` BEFORE we allocate * — the user sees a clean error, the worker doesn't OOM. */ export declare const MAX_PDF_BYTES: number; export declare const MAX_XML_BYTES: number; /** * Bounded binary document response. Always base64-encoded because MCP * `structuredContent` is JSON-only — raw `Uint8Array` would coerce to a * sparse object on the wire. */ export interface BinaryDocument { /** Echoed id from the request (invoice id), so callers can correlate. */ id: string; /** Verbatim `Content-Type` from the response (e.g. `application/pdf`). */ contentType: string; /** Byte length of the decoded body. Equal to the decoded base64 byte length. */ sizeBytes: number; /** Base64-encoded bytes. */ base64: string; /** Filename hint parsed from `Content-Disposition`, when present. */ filename?: string; } /** * Bounded XML document response (UBL / CII / Facturae / PEPPOL / FatturaPA / * XRechnung — anything declared `application/xml` or `text/xml`). */ export interface XmlDocument { /** Echoed id from the request (invoice id). */ id: string; /** Strictly decoded UTF-8 XML text. */ xml: string; /** Verbatim `Content-Type` from the response. */ contentType: string; /** Byte length of the decoded UTF-8 body. */ sizeBytes: number; /** Filename hint parsed from `Content-Disposition`, when present. */ filename?: string; } /** `/invoices/:id/xml` serves XML or a Factur-X PDF, depending on storage MIME. */ export type EInvoiceDocument = XmlDocument | BinaryDocument; export declare class FrihetApiError extends Error { readonly statusCode: number; readonly errorCode: string; readonly detail?: string | undefined; constructor(statusCode: number, errorCode: string, message?: string, detail?: string | undefined); } export interface FrihetClientOptions { /** * Per-request timeout in milliseconds. Defaults to 30000. * Cloudflare Workers should pass ≤25000 to leave margin under the ~30s limit. */ timeoutMs?: number; /** * Internal second factor for API keys provisioned by Frihet OAuth Workers. * Ordinary dashboard/API keys must omit it. The value is sent only to the * already-normalized API origin and is never included in logs or errors. */ oauthServiceSecret?: string; } export declare class FrihetClient { private readonly apiKey; private readonly baseUrl; private readonly timeoutMs; private readonly oauthServiceSecret?; constructor(apiKey: string, baseUrl?: string, options?: FrihetClientOptions); private request; /** Fetch a raw response. The caller owns timeout and body consumption. */ private fetchRaw; /** Consume one response body without ever retaining more than `maxBytes`. */ private readBoundedBody; /** * Bounded document fetch — ONE call, ONE Response, dispatch on Content-Type. * * Success and error bodies use the same bounded reader. The abort timer stays * active until the body is complete, and a normal success performs one GET. * A 429 may retry, matching the generic JSON client path. */ private requestDocument; private sleep; /** * Wrapper for endpoints whose backend wraps the payload in a `{ data, meta }` * envelope. This is the UNIFORM convention across the Frihet `/v1` REST API: * - single-object GET reads (`getResource` → `{ data: , meta }`), * - create/update mutations (201 / PUT / PATCH → `{ data: , meta }`, * publicApi.ts response block), AND * - action POSTs (`/invoices/:id/paid`, `/send`, `/credit-note`, deposit * apply/refund, etc. → `{ data: , meta }`, publicApi.ts * `actionResponse = { data: actionResult, meta }`). * * Passing that envelope straight into a tool's `structuredContent` surfaces * `{ data, meta }` instead of the resource/action result, breaking the tool's * output schema (id/clientName/items/success all read as `undefined`). This * unwraps to `body.data`. Every create/update/action mutation routes through * here for exactly this reason; single-object reads (`getInvoice`/…) have since * #64. Most `deleteX` methods keep `request` (they return `void` — the tool * discards the body and synthesizes `{ success, id }`, so there is nothing to * unwrap). `deleteInvoice`/`deleteQuote` are the EXCEPTION and route through * here: their backend answers 204 for a real delete but 200 + `{ data, meta }` * when it soft-CANCELS a non-draft document instead (see those methods). * Only unwraps a non-array-object `data` (see guard below), so an * array-`data` list envelope and any non-enveloped body pass through unchanged. * A 204 yields `undefined` from `request`, which falls through untouched — that * `undefined` is what tells the delete tools "destroyed, not cancelled". * * Only unwraps when the body is an object carrying a `data` property that is * itself a (non-array) object — i.e. a genuine single-object envelope. It is * deliberately distinct from {@link requestPaginated}, which keeps the * `{ data: [...] , meta }` shape intact for list endpoints. If the body is * not an envelope (legacy endpoints that return the item directly), it is * returned unchanged so existing callers keep working. */ private requestUnwrapped; /** Wrapper for paginated endpoints — validates response shape has `data` array. */ private requestPaginated; listInvoices(params?: { limit?: number; offset?: number; after?: string; fields?: string; status?: string; from?: string; to?: string; clientId?: string; seriesId?: string; }): Promise>>; getInvoice(id: string): Promise>; createInvoice(data: Record): Promise>; updateInvoice(id: string, data: Record): Promise>; /** * DELETE /invoices/{id} — NOT an unconditional destroy. * * The backend refuses to destroy a non-draft invoice (VeriFactu hash-chain * integrity) and soft-CANCELS it instead, and it distinguishes the two * outcomes on the wire (`publicApi.ts` DELETE branch): * - 204 No Content → the draft row was really removed * - 200 + `{ data: { id, status: 'cancelled', previousStatus, * cancelledVia }, meta }` → the document still exists, now cancelled * * This used to be typed `Promise` and the body was thrown away, so the * tool reported "deleted successfully" for a document that was still there. * Resolves to the unwrapped soft-cancel payload on 200, `undefined` on 204. */ deleteInvoice(id: string): Promise | undefined>; searchInvoices(query: string, params?: { limit?: number; offset?: number; after?: string; fields?: string; status?: string; from?: string; to?: string; }): Promise>>; listExpenses(params?: { limit?: number; offset?: number; after?: string; fields?: string; from?: string; to?: string; vendorId?: string; category?: string; }): Promise>>; getExpense(id: string): Promise>; createExpense(data: Record): Promise>; updateExpense(id: string, data: Record): Promise>; deleteExpense(id: string): Promise; listClients(params?: { limit?: number; offset?: number; after?: string; fields?: string; q?: string; stage?: string; }): Promise>>; getClient(id: string): Promise>; createClient(data: Record): Promise>; updateClient(id: string, data: Record): Promise>; deleteClient(id: string): Promise; listProducts(params?: { limit?: number; offset?: number; after?: string; fields?: string; q?: string; isActive?: boolean; }): Promise>>; getProduct(id: string): Promise>; createProduct(data: Record): Promise>; updateProduct(id: string, data: Record): Promise>; deleteProduct(id: string): Promise; listQuotes(params?: { limit?: number; offset?: number; after?: string; fields?: string; status?: string; from?: string; to?: string; clientId?: string; seriesId?: string; }): Promise>>; getQuote(id: string): Promise>; createQuote(data: Record): Promise>; updateQuote(id: string, data: Record): Promise>; /** * DELETE /quotes/{id} — same two-outcome contract as {@link deleteInvoice}: * the backend soft-CANCELS a non-draft quote (200 + body), destroys only a * clean draft with no delivery/response/attachment/conversion evidence (204), * and refuses a protected draft with 409. */ deleteQuote(id: string): Promise | undefined>; listVendors(params?: { q?: string; limit?: number; offset?: number; after?: string; fields?: string; }): Promise>>; getVendor(id: string): Promise>; createVendor(data: Record): Promise>; updateVendor(id: string, data: Record): Promise>; deleteVendor(id: string): Promise; sendInvoice(id: string, to?: string): Promise>; markInvoicePaid(id: string, paidDate?: string): Promise>; getInvoicePdf(id: string): Promise; getInvoiceEInvoice(invoiceId: string): Promise; /** * `POST /v1/invoices/:id/credit-note` — creates a rectificativa DRAFT. * * The backend REQUIRES an `Idempotency-Key` header (`400 * IDEMPOTENCY_KEY_REQUIRED` without it). `request` mints one for every * mutation, so passing `idempotencyKey` is optional: supply it to make a * caller-driven retry replay the stored 201 instead of creating a second * draft. The backend marks that replay with `X-Idempotent-Replayed: true`, * but this client reads no response headers, so the replayed 201 and the * original are indistinguishable to the caller — both are the same draft, * which is the property that matters here. */ createCreditNote(invoiceId: string, data: { reason: string; reasonDescription?: string; fullCredit?: boolean; issueDate?: string; }, idempotencyKey?: string): Promise>; applyLateFee(invoiceId: string, data?: { amount?: number; daysOverdue?: number; }): Promise; sendQuote(id: string, to?: string): Promise>; listWebhooks(): Promise<{ data: Webhook[]; total: number; }>; getWebhook(id: string): Promise; createWebhook(data: CreateWebhookInput): Promise; updateWebhook(id: string, data: UpdateWebhookInput): Promise; deleteWebhook(id: string): Promise; listClientContacts(clientId: string, params?: { limit?: number; offset?: number; }): Promise>>; createClientContact(clientId: string, data: Record): Promise>; deleteClientContact(clientId: string, contactId: string): Promise; listClientActivities(clientId: string, params?: { limit?: number; offset?: number; }): Promise>>; logClientActivity(clientId: string, data: Record): Promise>; listClientNotes(clientId: string, params?: { limit?: number; offset?: number; }): Promise>>; createClientNote(clientId: string, data: Record): Promise>; deleteClientNote(clientId: string, noteId: string): Promise; listDeposits(params?: { limit?: number; offset?: number; after?: string; fields?: string; from?: string; to?: string; clientId?: string; status?: string; }): Promise>>; getDeposit(id: string): Promise>; createDeposit(data: Record): Promise>; updateDeposit(id: string, data: Record): Promise>; deleteDeposit(id: string): Promise; applyDeposit(id: string, data?: Record): Promise>; refundDeposit(id: string, data?: Record): Promise>; sendEInvoice(params: { invoiceId: string; format: string; dispatchMode: string; }): Promise<{ workflowRunId: string; status: "queued"; estimatedCompletionSec: number; }>; getEInvoiceStatus(workflowRunId: string): Promise<{ status: "queued" | "running" | "succeeded" | "failed" | "cancelled"; step: string; error?: string; ackId?: string; pdfA3Url?: string; xmlUrl?: string; }>; validateEInvoiceXml(params: { xml: string; format: string; }): Promise<{ valid: boolean; errors: Array<{ severity: string; location: string; message: string; rule: string; }>; validator: "kosit" | "mustang" | "xsd" | "schematron"; durationMs: number; }>; exportDatev(params: { periodStart: string; periodEnd: string; format: string; }): Promise<{ fileUrl: string; filename: string; rowCount: number; fiscalPeriod: string; encoding: "cp1252"; }>; exportEInvoice(params: { invoiceId: string; format: string; signed?: boolean; }): Promise<{ xmlUrl: string; filename: string; format: string; signed: boolean; }>; faceSubmit(params: { invoiceId: string; mode: "mock" | "sandbox" | "production"; }): Promise<{ registroFACe: string; status: "submitted" | "error"; submittedAt: string; mode: string; }>; faceStatus(params: { invoiceId: string; }): Promise<{ registroFACe: string; statusCode: string; statusDescription: string; rejectionReason?: string; }>; ticketbaiSubmit(params: { invoiceId: string; sandbox: boolean; }): Promise<{ tbaiId: string; territory: "bizkaia" | "gipuzkoa" | "araba"; status: "submitted" | "accepted" | "rejected" | "error"; sandbox: boolean; qrUrl?: string; }>; ticketbaiStatus(params: { invoiceId: string; }): Promise<{ tbaiId: string; territory: "bizkaia" | "gipuzkoa" | "araba"; status: "submitted" | "accepted" | "rejected" | "error"; rejectionReason?: string; error?: string; }>; listReservations(params?: { propertyId?: string; status?: string; checkInFrom?: string; checkInTo?: string; fields?: string; limit?: number; offset?: number; after?: string; }): Promise>>; getReservation(id: string): Promise>; createReservation(data: Record): Promise>; listProperties(params?: { q?: string; isActive?: boolean; fields?: string; limit?: number; offset?: number; after?: string; }): Promise>>; syncChannel(channelId: string, direction: "pull" | "push" | "both"): Promise>; listTerminals(params?: { locationId?: string; limit?: number; offset?: number; }): Promise>>; getSale(id: string): Promise>; listSales(params?: { terminalId?: string; status?: string; from?: string; to?: string; limit?: number; offset?: number; after?: string; }): Promise>>; refundSale(id: string, data?: { amountCents?: number; reason?: string; }): Promise>; listKitchenTickets(params?: { status?: string; stationId?: string; limit?: number; offset?: number; after?: string; }): Promise>>; getKitchenTicket(id: string): Promise>; updateKitchenTicket(id: string, data: Record): Promise>; listKitchenStations(params?: { limit?: number; offset?: number; }): Promise>>; listMenuItems(params?: { q?: string; isActive?: boolean; limit?: number; offset?: number; after?: string; }): Promise>>; getBusinessContext(): Promise>; getMonthlySummary(month?: string): Promise>; getQuarterlyTaxes(quarter?: string): Promise>; listBankAccounts(params?: { limit?: number; offset?: number; }): Promise>>; getBankAccount(id: string): Promise>; listTransactions(params?: { accountId?: string; from?: string; to?: string; status?: string; category?: string; limit?: number; offset?: number; after?: string; }): Promise>>; categorizeTransaction(id: string, data: { category: string; notes?: string; }): Promise>; matchTransactionToDocument(transactionId: string, data: { documentId: string; documentType: "invoice" | "expense"; notes?: string; }): Promise>; getFiscalModeloSummary(modeloCode: string, period?: string): Promise>; getVerifactuStatus(invoiceId: string): Promise>; resubmitVerifactu(invoiceId: string): Promise>; getTicketbaiStatus(invoiceId: string): Promise>; listTimeEntries(params?: { userId?: string; projectId?: string; from?: string; to?: string; billable?: boolean; limit?: number; offset?: number; after?: string; }): Promise>>; getTimeEntry(id: string): Promise>; createTimeEntry(data: Record): Promise>; updateTimeEntry(id: string, data: Record): Promise>; deleteTimeEntry(id: string): Promise; getTimeSummary(params: { from: string; to: string; userId?: string; projectId?: string; groupBy?: string; }): Promise>; listRecurringInvoices(params?: { status?: string; limit?: number; offset?: number; }): Promise>>; getRecurringInvoice(id: string): Promise>; createRecurringInvoice(data: Record): Promise>; updateRecurringInvoice(id: string, data: Record): Promise>; pauseRecurringInvoice(id: string): Promise>; resumeRecurringInvoice(id: string): Promise>; deleteRecurringInvoice(id: string): Promise; runRecurringNow(templateId: string, options?: { draftOnly?: boolean; }): Promise>; listTeamMembers(params?: { role?: string; status?: string; limit?: number; offset?: number; }): Promise>>; inviteTeamMember(data: { email: string; role: string; name?: string; }): Promise>; updateTeamMemberRole(memberId: string, role: string): Promise>; removeTeamMember(memberId: string): Promise; sendGestoriaMessage(data: { workspaceId: string; parentType: "documentRequest" | "filingItem" | "obligation"; parentId: string; body: string; }): Promise>; listGestoriaMessages(params: { workspaceId: string; parentType: "documentRequest" | "filingItem" | "obligation"; parentId: string; limit?: number; before?: string; }): Promise<{ messages: Array>; hasMore: boolean; }>; createGestoriaTemplate(data: { name: string; title: string; description: string; dueDateOffsetDays: number; attachmentRequired?: boolean; variables?: Array<{ key: string; label?: string; defaultValue?: string; }>; }): Promise<{ templateId: string; }>; bulkSendGestoriaTemplate(data: { templateId: string; clientWorkspaceIds: string[]; periodOverrides?: { quarter?: string | number; year?: string | number; month?: string | number; }; }): Promise>; getGestoriaAgingConsolidated(params?: { ownerUid?: string; }): Promise>; approveGLEntry(entryId: string, notes?: string): Promise>; rejectGLEntry(entryId: string, reason: string): Promise>; getGLEntryAuditLog(entryId: string): Promise>; addCustomPortalDomain(data: { domain: string; workspaceId?: string; }): Promise>; verifyCustomPortalDomain(data: { domain: string; }): Promise>; removeCustomPortalDomain(data: { domain: string; }): Promise>; generatePortalOnboardLink(data: { email: string; name?: string; expiresInHours?: number; workspaceId?: string; }): Promise>; lookupTaxIdViaVIES(data: { vatNumber: string; countryCode: string; }): Promise>; getIgicModeloSummary(modeloCode: string, params?: { year?: string; period?: string; }): Promise>; calculateAiem(data: { ncCode: string; amount: number; description?: string; }): Promise>; getISSummary(modeloCode: string, params?: { year?: string; installment?: string; }): Promise>; listBankRules(params?: { isActive?: boolean; limit?: number; offset?: number; }): Promise>>; createBankRule(data: { name: string; conditions: Array<{ field: string; operator: string; value: string; }>; actions: Array<{ type: string; value: string; }>; isActive?: boolean; }): Promise>; listLeaves(params?: { employeeId?: string; status?: string; from?: string; to?: string; limit?: number; offset?: number; after?: string; }): Promise>>; createLeaveRequest(data: { employeeId: string; type: string; startDate: string; endDate: string; reason?: string; }): Promise>; approveLeave(leaveId: string, data?: { reason?: string; }): Promise>; rejectLeave(leaveId: string, data: { reason: string; }): Promise>; cancelLeave(leaveId: string): Promise>; attendanceClockIn(data: { employeeId: string; mood?: string; location?: string; }): Promise>; attendanceClockOut(entryId: string): Promise>; getOvertimeReport(params: { period: string; employeeId?: string; }): Promise>; listAnomalies(params?: { type?: string; severity?: string; from?: string; to?: string; limit?: number; offset?: number; }): Promise>>; testWebhook(id: string, data?: { eventType?: string; }): Promise>; exportPayroll(params: { format: "a3" | "contasol" | "sage" | "siltra"; month: string; }): Promise>; getPayrollChecklist(params: { month: string; }): Promise>; getOnboardingStatus(): Promise>; setOnboardingPersona(data: { persona: "autonomo" | "empresa" | "agencia" | "gestoria"; }): Promise>; getPermissionsMatrix(): Promise>; getMyPermissions(): Promise>; getCurrentPeriod(params?: { fiscalYear?: string; }): Promise>; closePeriod(data: { type: "monthly" | "quarterly"; }): Promise>; reopenPeriod(data: { periodId: string; reason: string; }): Promise>; } //# sourceMappingURL=client.d.ts.map