/** * Copyright (c) 2026 AzerothJS. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import type { Issue } from '@azerothjs/schema'; import type { Decl, Feature, Manifest, PathParams } from './declare.ts'; /** The error a failed call throws: the wire shape, typed. */ export declare class ApiError extends Error { /** The HTTP status. */ readonly status: number; /** The stable machine-readable code from the wire shape ('validation-failed', ...). */ readonly code: string; /** The field-path error map of a validation failure - feed it to the form's setError. */ readonly fields: Record; /** * The per-issue detail of a validation failure: path, machine code, message - the ONE issue * shape the whole framework speaks. Lifted out of `details` alongside `fields` so code that * branches on an issue CODE rather than a message does not have to reach through an `unknown`. */ readonly issues: ReadonlyArray; /** The full `error.details` payload, for anything beyond code/message/fields/issues. */ readonly details: unknown; constructor(status: number, code: string, message: string, details: unknown); } /** @internal Empty-object detection for argument optionality. */ type IsEmpty = keyof T extends never ? true : false; /** The argument object one call takes - only the parts its route declared. */ export type CallArgs = (IsEmpty> extends true ? unknown : { params: PathParams; }) & (undefined extends In ? unknown : { input: In; }) & (undefined extends Query ? unknown : { query: Query; }); /** * The wire projection of a declared type. The manifest carries no schemas, so the client * cannot revive what JSON flattened: a declared `Date` arrives as its ISO string, and the * call's return type says so instead of lying. Everything JSON keeps intact maps to itself. */ export type Wire = T extends Date ? string : T extends ReadonlyArray ? Array> : T extends object ? { [K in keyof T]: Wire; } : T; /** * One route as a client call. A route that declared no params, input or query takes NO argument - * so `client.health.ping()` rather than `client.health.ping({})` - and every other route takes * exactly the parts it declared, which is what makes a forgotten `input` a compile error. */ export type Call = IsEmpty & object> extends true ? () => Promise> : (args: CallArgs) => Promise>; /** * A server action as a client call: the input object IS the whole argument (an action path * carries no params by declaration), so `client.posts.create({ title })` - no args wrapper. */ export type ActionCall = undefined extends In ? () => Promise> : (input: In) => Promise>; /** * One feature as a client namespace: its JSON routes become calls (the full path - feature * prefix included - types the params); form/raw/stream routes are filtered OUT at the type * level, and the manifest's `kind` marker backs that with a loud runtime refusal. */ export type FeatureClient = F extends Feature ? { [K in keyof R as R[K] extends Decl, 'json' | 'action'> ? K : R[K] extends Decl ? K : never]: R[K] extends Decl ? Call<`${Prefix}${P}`, In, Out, Query> : R[K] extends Decl ? ActionCall : never; } : never; /** The whole registered record as a typed client surface: `client..(...)`. */ export type ClientOf> = { [G in keyof Features]: FeatureClient; }; /** How {@link createClient} reaches the server: the base URL, an optional transport, headers. */ export interface ClientOptions { /** * Where the API is registered, e.g. '/api' or 'https://host/api'. Absolute means a scheme * (`https:`, in any case) or a leading `//`, and an absolute one always goes over the wire; * anything else is relative and means "this origin". */ baseUrl: string; /** * The transport, and the first case of the selection above: it wins over everything, * including the in-process bridge, and keeps the inert base a relative baseUrl has always * resolved against. Pass an App's `handle` for in-process tests. */ fetch?: (request: Request) => Promise; /** Headers added to every call (auth tokens live here). */ headers?: Record; /** * Largest response body to read, in bytes (default 1 MiB, matching the server's own default). * An unbounded `response.json()` is a memory-exhaustion primitive handed to whatever answered. */ maxResponseBytes?: number; /** * CSRF auto-header for ACTION calls: in a browser the client mirrors the readable token * cookie (`__Host-azcsrf`, then `azcsrf`) into `x-azeroth-csrf` automatically. Pass * names to match a renamed `csrfCookie`/`csrfProtect` pair, or `false` to disable. */ csrf?: false | { cookie?: string; header?: string; }; } /** * Builds the typed client: the {@link Manifest} supplies each route's method and path, `typeof` * the server's features supplies every type. The two come from the same declaration - the * manifest is the framework's own projection of it, not a hand-written mirror. * * ```ts * // import type { api } from the server module that called register() * const client = createClient(manifest, { baseUrl: '/api' }); * const key = await client.keys.create({ input: { label: 'ci' } }); * ``` */ export declare function createClient>(manifest: Manifest, options: ClientOptions): ClientOf; /** * Lands an {@link ApiError}'s field map on a form. Nested wire paths land on their first * dot segment - `items.0.email` on `items` - matching the form's own schema-overlay rule; * the first message per field wins. Returns false (form untouched) for anything that is * not an ApiError carrying fields, so the caller keeps one honest branch for "the server * did not speak validation": * * ```ts * catch (error) * { * if (!applyFieldErrors(form, error)) * { * form.setError('title', 'Could not reach the server - try again.'); * } * } * ``` * * `setError` is declared in METHOD syntax deliberately: method parameters check * bivariantly, which is what lets a FormApi whose setError takes `keyof T` assign here. */ export declare function applyFieldErrors(form: { setError(name: string, message: string | null): void; }, error: unknown): boolean; export {};