/** * Typed platform client — every path, parameter and body the OpenAPI specs * describe, checked at compile time. * * import { createApiClient } from "@nullplatform/plugin/package"; * * const api = createApiClient(); * const { data, error } = await api.GET("/service/{id}", { params: { path: { id } } }); * await api.PATCH("/service/{id}", { params: { path: { id } }, body: { status: "active" } }); * * The `paths` of every spec in `js/openapi/` (generated into `./generated/` * by `bun run generate:api`) intersect into one surface, so a path the * platform does not expose is a type error rather than a runtime 404. * * Authentication is the SDK's usual one: NP_API_KEY is exchanged for a * bearer token (src/auth.ts) and cached until shortly before it expires. A * warm worker's token can still lapse between calls, so a 401 forces one * fresh exchange and a single retry — the same rule the untyped helpers in * src/api.ts follow. */ import createClient, { type Middleware } from "openapi-fetch"; import { accessToken } from "../auth"; import { cfg } from "../config"; import type { paths as ApiSpecPaths } from "./generated/api-spec"; import type { paths as ApprovalPaths } from "./generated/approval"; import type { paths as AuthenticationPaths } from "./generated/authentication"; import type { paths as AuthorizationPaths } from "./generated/authorization"; import type { paths as ControlplanePaths } from "./generated/controlplane"; import type { paths as EntityHooksPaths } from "./generated/entity-hooks"; import type { paths as NotificationsPaths } from "./generated/notifications"; import type { paths as NrnPaths } from "./generated/nrn"; import type { paths as ParametersPaths } from "./generated/parameters"; import type { paths as ProvidersPaths } from "./generated/providers"; import type { paths as RuntimeConfigPaths } from "./generated/runtime-config"; import type { paths as ScopePaths } from "./generated/scope"; import type { paths as ServicesPaths } from "./generated/services-api"; import type { paths as UsersPaths } from "./generated/users"; /** Every platform path, across all specs. No two specs define the same path. */ export type PlatformPaths = ApiSpecPaths & ApprovalPaths & AuthenticationPaths & AuthorizationPaths & ControlplanePaths & EntityHooksPaths & NotificationsPaths & NrnPaths & ParametersPaths & ProvidersPaths & RuntimeConfigPaths & ScopePaths & ServicesPaths & UsersPaths; export interface ApiClientOptions { /** Platform API root. Default: `cfg.apiUrl()` (NP_API_URL). */ baseUrl?: string; /** * Bearer token provider. Default: the SDK's NP_API_KEY exchange. Called * again with `force = true` after a 401, when a provider that caches should * mint a fresh token; a provider that ignores the flag simply retries once. */ token?: (force?: boolean) => Promise; /** fetch override — lets tests record calls without a network. */ fetch?: typeof fetch; } export function createApiClient(opts: ApiClientOptions = {}) { // Resolved per call so a fetch swapped in later (tests do this) is honoured. const send = (request: Request) => (opts.fetch ?? globalThis.fetch)(request); const token = opts.token ?? ((force?: boolean) => accessToken(force, opts.fetch)); const client = createClient({ baseUrl: opts.baseUrl ?? cfg.apiUrl(), fetch: opts.fetch, }); // A clone of each in-flight request, body intact, so a 401 can be replayed // once with a fresh token. Dropped as soon as the response (or error) lands. const replayable = new Map(); const auth: Middleware = { async onRequest({ id, request }) { replayable.set(id, request.clone()); request.headers.set("Authorization", `Bearer ${await token(false)}`); return request; }, async onResponse({ id, response }) { const retry = replayable.get(id); replayable.delete(id); if (response.status !== 401 || !retry) return response; retry.headers.set("Authorization", `Bearer ${await token(true)}`); return send(retry); }, onError({ id }) { replayable.delete(id); }, }; client.use(auth); return client; } type PlatformClient = ReturnType; /** The typed platform client. An interface, so two contexts that carry it compare by identity instead of by its (very large) structure. */ export interface ApiClient extends PlatformClient {}