// The typed management API for the Promise facade (RFC §7): every REST endpoint of the host as // an autocompletable method with checked request/response types — named exactly as in the // OpenAPI document (`listPairedClients`, `stopSession`, `reconcileProviderEntries`, …). This is // the typed front door; `pf.request(method, path, body)` stays as the untyped escape hatch. // // It's a thin, zero-drift veneer over the generated client (`./gen/punktfunk.ts`, from // `@effect/openapi-generator`): the generated methods return Effects, so each is run to a Promise // here. The transport is the SAME CA-pinning fetch the rest of the SDK uses (config.ts), fed to // Effect's `HttpClient` via the overridable `FetchHttpClient.Fetch` reference — so the loopback // pin is preserved and there is still exactly one place that knows how to reach the host. import { Effect } from "effect"; import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; import type { ResolvedConfig } from "./config.js"; import * as gen from "./gen/punktfunk.js"; /** * Make a trailing argument that merely *accepts* `undefined` genuinely optional. The generated * methods type their options bag as `{…} | undefined` (a required param that tolerates `undefined`), * so without this you'd have to write `listPairedClients(undefined)`. Endpoints with a required * body (`{ payload }`, not `undefined`-able) are left untouched. */ type OptionalizeTail = A extends readonly [ ...infer Init, infer Last, ] ? undefined extends Last ? [...Init, Last?] : A : A; /** Each generated method returns an Effect; expose it as a Promise of the decoded success value. */ type Promiseify = T extends ( ...args: infer A ) => Effect.Effect ? (...args: OptionalizeTail) => Promise : never; /** The event stream (an Effect `Stream`) and the void catch-all — served by `pf.events` instead. */ type NonApiMethods = "httpClient" | "streamEvents" | "streamEventsSse"; /** * The host's management API, fully typed: `await pf.api.listPairedClients()` gives a typed array, * `await pf.api.reconcileProviderEntries("romm", { payload })` checks the body at compile time — * no hand-written paths, no `unknown` casts. A failing call rejects with the endpoint's typed * error. The live-event SSE endpoint is intentionally absent — subscribe via `pf.events`. */ export type HostApi = { readonly [K in Exclude]: Promiseify< gen.Punktfunk[K] >; }; /** Build the pinning `HttpClient`: base URL + bearer + the config's CA-pinning fetch. */ const httpClientFor = (cfg: ResolvedConfig): Promise => Effect.runPromise( HttpClient.HttpClient.pipe( Effect.map((client) => HttpClient.mapRequest((request: HttpClientRequest.HttpClientRequest) => request.pipe( HttpClientRequest.prependUrl(cfg.url), HttpClientRequest.bearerToken(cfg.token), HttpClientRequest.acceptJson, ), )(client), ), Effect.provide(FetchHttpClient.layer), // Override the default `globalThis.fetch` with the CA-pinning one (config.ts). Effect.provideService(FetchHttpClient.Fetch, cfg.fetch), ), ); /** Skipped at runtime too (not just in the type): these aren't Promise-shaped API calls. */ const NON_API: ReadonlySet = new Set([ "httpClient", "streamEvents", "streamEventsSse", ]); /** The typed API surface over a resolved connection — each method runs its Effect to a Promise. */ export const makeHostApi = async (cfg: ResolvedConfig): Promise => { const client = gen.make(await httpClientFor(cfg)) as unknown as Record< string, unknown >; const api: Record = {}; for (const key of Object.keys(client)) { if (NON_API.has(key)) continue; const method = client[key]; if (typeof method !== "function") continue; api[key] = (...args: unknown[]) => Effect.runPromise( (method as (...a: unknown[]) => Effect.Effect)(...args), ); } return api as HostApi; };