@c9up/aurora 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,7 @@ providers: [
24
24
 
25
25
  ## Entry points
26
26
 
27
- - `@c9up/aurora` — main API
27
+ - `@c9up/aurora` — main API: reactive primitives (`signal`/`effect`/`html`/`component`/`hydrate`) plus the client toolkit — `WebStorage`/`persistedSignal`, reactive browser signals (`prefersDark`/`online`/`windowSize`/…), SPA navigation (`navigate`/`queryParam`), `cookie`/`clipboard`/`share`, and the `HttpClient` fetch wrapper
28
28
  - `@c9up/aurora/provider` — Ream IoC provider
29
29
  - `@c9up/aurora/services/main` — container service accessor
30
30
  - `@c9up/aurora/relay` — realtime adapter
package/dist/browser.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * without `typeof window` guards at every call site. Node-free — part of the
7
7
  * client barrel.
8
8
  */
9
+ import { type Signal } from "./reactive.js";
9
10
  /** Navigate to `url` with a full page load. No-op during SSR. */
10
11
  export declare function redirect(url: string): void;
11
12
  /**
@@ -15,14 +16,145 @@ export declare function redirect(url: string): void;
15
16
  export declare function replace(url: string): void;
16
17
  /** Reload the current page. No-op during SSR. */
17
18
  export declare function reload(): void;
19
+ /** Which Web Storage area backs a {@link WebStorage}. */
20
+ export type StorageArea = "local" | "session";
21
+ export interface WebStorageOptions {
22
+ /**
23
+ * Key namespace, e.g. `"myapp:"`. Keys are written and read prefixed so
24
+ * independent stores never collide. Default `""` (no namespace).
25
+ */
26
+ prefix?: string;
27
+ /**
28
+ * Backing area — `"local"` (`localStorage`, persists across sessions, the
29
+ * default) or `"session"` (`sessionStorage`, cleared when the tab closes).
30
+ */
31
+ area?: StorageArea;
32
+ }
18
33
  /**
19
- * Typed, SSR-safe `localStorage` wrapper. Values are JSON-serialised; reads
20
- * return `null` on the server, on a missing key, or on malformed JSON. Writes
21
- * swallow quota / private-mode errors so a full storage never crashes the app.
34
+ * Typed, SSR-safe key/value store over `localStorage` / `sessionStorage`.
35
+ *
36
+ * - Values are JSON-serialised; reads return `null` on the server, on a missing
37
+ * key, or on malformed JSON.
38
+ * - Writes swallow quota / private-mode errors so a full store never crashes
39
+ * the app (best-effort).
40
+ * - `prefix` namespaces keys; {@link keys} and {@link clear} stay scoped to it,
41
+ * so two prefixed stores over the same area never touch each other's data.
22
42
  */
23
- export declare const storage: {
43
+ export declare class WebStorage {
44
+ #private;
45
+ constructor(options?: WebStorageOptions);
46
+ /** The on-disk key for `key`, namespaced by the configured prefix. */
47
+ fullKey(key: string): string;
24
48
  get<T>(key: string): T | null;
25
49
  set(key: string, value: unknown): void;
50
+ /** Whether `key` is present (and not the server). */
51
+ has(key: string): boolean;
26
52
  remove(key: string): void;
53
+ /** Read `key`, or compute + persist `factory()` on a miss, returning the value. */
54
+ getOrSet<T>(key: string, factory: () => T): T;
55
+ /** Keys in this store, prefix stripped. Empty array during SSR. */
56
+ keys(): string[];
57
+ /** Remove this store's keys. With no prefix this clears the whole area. */
27
58
  clear(): void;
59
+ }
60
+ /**
61
+ * Default `localStorage` store (no namespace). Backward-compatible with the
62
+ * previous `storage.get/set/remove/clear` helper, plus `has`/`keys`/`getOrSet`.
63
+ */
64
+ export declare const storage: WebStorage;
65
+ /** Default `sessionStorage` store (no namespace) — cleared when the tab closes. */
66
+ export declare const session: WebStorage;
67
+ export interface PersistedSignalOptions extends WebStorageOptions {
68
+ /**
69
+ * Update the signal when ANOTHER tab writes the same key (the browser
70
+ * `storage` event). Default `true`. Only effective for `area: "local"` —
71
+ * the browser never emits storage events for `sessionStorage` across tabs.
72
+ */
73
+ crossTab?: boolean;
74
+ }
75
+ /**
76
+ * A {@link Signal} whose value is mirrored to web storage.
77
+ *
78
+ * The initial value is read from storage (falling back to `initial` on a miss)
79
+ * and every write is persisted via a reactive `effect`. With `crossTab`
80
+ * (default `true`, local area only) the signal also updates live when another
81
+ * tab writes the same key. SSR-safe: with no `window` it is a plain in-memory
82
+ * signal seeded with `initial`. Because the mirror runs inside `effect`, a
83
+ * `persistedSignal` created in a component's setup is disposed with it.
84
+ */
85
+ export declare function persistedSignal<T>(key: string, initial: T, options?: PersistedSignalOptions): Signal<T>;
86
+ /** Reactive `window.matchMedia(query).matches`. `false` during SSR. */
87
+ export declare function mediaQuery(query: string): Signal<boolean>;
88
+ /** Reactive `prefers-color-scheme: dark`. Shortcut over {@link mediaQuery}. */
89
+ export declare function prefersDark(): Signal<boolean>;
90
+ /** Reactive online/offline status (`navigator.onLine`). `true` during SSR. */
91
+ export declare function online(): Signal<boolean>;
92
+ export interface WindowSize {
93
+ width: number;
94
+ height: number;
95
+ }
96
+ /** Reactive `{ width, height }` of the viewport. `{0,0}` during SSR. */
97
+ export declare function windowSize(): Signal<WindowSize>;
98
+ /** Reactive tab visibility (`!document.hidden`). `true` during SSR. */
99
+ export declare function visibility(): Signal<boolean>;
100
+ /** Reactive `window.location.hash`. `""` during SSR. */
101
+ export declare function hash(): Signal<string>;
102
+ /** Go back one history entry. No-op during SSR. */
103
+ export declare function back(): void;
104
+ /** Go forward one history entry. No-op during SSR. */
105
+ export declare function forward(): void;
106
+ /**
107
+ * SPA navigation: push `url` onto history WITHOUT a full page reload (contrast
108
+ * {@link redirect}, which reloads). Emits a `popstate` event so reactive URL
109
+ * consumers — e.g. {@link queryParam} or a router — pick up the change. No-op
110
+ * during SSR.
111
+ */
112
+ export declare function navigate(url: string): void;
113
+ /**
114
+ * A {@link Signal} bound to a single URL query parameter. Reading reflects the
115
+ * current value (`null` when absent); writing updates the URL via `pushState`
116
+ * (no reload). Stays in sync with back/forward and {@link navigate} through the
117
+ * `popstate` event. SSR-safe: a plain `null` signal with no listeners.
118
+ */
119
+ export declare function queryParam(key: string): Signal<string | null>;
120
+ export interface CookieOptions {
121
+ /** Path scope. Default `"/"`. */
122
+ path?: string;
123
+ /** Lifetime in seconds. */
124
+ maxAge?: number;
125
+ /** Absolute expiry. */
126
+ expires?: Date;
127
+ /** Domain scope. */
128
+ domain?: string;
129
+ /** SameSite policy. */
130
+ sameSite?: "strict" | "lax" | "none";
131
+ /** Restrict to HTTPS. */
132
+ secure?: boolean;
133
+ }
134
+ /**
135
+ * SSR-safe cookie accessor — the one store {@link WebStorage} can't cover, for
136
+ * values the server also reads. Reads return `null` during SSR; writes are a
137
+ * no-op. Names and values are URL-encoded.
138
+ */
139
+ export declare const cookie: {
140
+ get(name: string): string | null;
141
+ set(name: string, value: string, options?: CookieOptions): void;
142
+ remove(name: string, options?: Pick<CookieOptions, "path" | "domain">): void;
28
143
  };
144
+ /** Async clipboard access. Methods return `false`/`null` when unavailable. */
145
+ export declare const clipboard: {
146
+ /** Copy `text`. Returns whether it succeeded. */
147
+ copy(text: string): Promise<boolean>;
148
+ /** Read clipboard text, or `null` if unavailable / denied. */
149
+ read(): Promise<string | null>;
150
+ };
151
+ export interface ShareData {
152
+ title?: string;
153
+ text?: string;
154
+ url?: string;
155
+ }
156
+ /**
157
+ * Invoke the native Web Share sheet. Returns `false` when unsupported or the
158
+ * user cancels — never throws.
159
+ */
160
+ export declare function share(data: ShareData): Promise<boolean>;
package/dist/browser.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * without `typeof window` guards at every call site. Node-free — part of the
7
7
  * client barrel.
8
8
  */
9
+ import { effect, signal } from "./reactive.js";
9
10
  /** Navigate to `url` with a full page load. No-op during SSR. */
10
11
  export function redirect(url) {
11
12
  if (typeof window !== "undefined") {
@@ -28,15 +29,38 @@ export function reload() {
28
29
  }
29
30
  }
30
31
  /**
31
- * Typed, SSR-safe `localStorage` wrapper. Values are JSON-serialised; reads
32
- * return `null` on the server, on a missing key, or on malformed JSON. Writes
33
- * swallow quota / private-mode errors so a full storage never crashes the app.
32
+ * Typed, SSR-safe key/value store over `localStorage` / `sessionStorage`.
33
+ *
34
+ * - Values are JSON-serialised; reads return `null` on the server, on a missing
35
+ * key, or on malformed JSON.
36
+ * - Writes swallow quota / private-mode errors so a full store never crashes
37
+ * the app (best-effort).
38
+ * - `prefix` namespaces keys; {@link keys} and {@link clear} stay scoped to it,
39
+ * so two prefixed stores over the same area never touch each other's data.
34
40
  */
35
- export const storage = {
41
+ export class WebStorage {
42
+ #prefix;
43
+ #area;
44
+ constructor(options = {}) {
45
+ this.#prefix = options.prefix ?? "";
46
+ this.#area = options.area ?? "local";
47
+ }
48
+ #backend() {
49
+ if (typeof window === "undefined")
50
+ return undefined;
51
+ return this.#area === "session"
52
+ ? window.sessionStorage
53
+ : window.localStorage;
54
+ }
55
+ /** The on-disk key for `key`, namespaced by the configured prefix. */
56
+ fullKey(key) {
57
+ return this.#prefix + key;
58
+ }
36
59
  get(key) {
37
- if (typeof localStorage === "undefined")
60
+ const backend = this.#backend();
61
+ if (!backend)
38
62
  return null;
39
- const raw = localStorage.getItem(key);
63
+ const raw = backend.getItem(this.fullKey(key));
40
64
  if (raw === null)
41
65
  return null;
42
66
  try {
@@ -45,25 +69,310 @@ export const storage = {
45
69
  catch {
46
70
  return null;
47
71
  }
48
- },
72
+ }
49
73
  set(key, value) {
50
- if (typeof localStorage === "undefined")
74
+ const backend = this.#backend();
75
+ if (!backend)
51
76
  return;
52
77
  try {
53
- localStorage.setItem(key, JSON.stringify(value));
78
+ backend.setItem(this.fullKey(key), JSON.stringify(value));
54
79
  }
55
80
  catch {
56
81
  // QuotaExceededError / Safari private mode — best-effort write.
57
82
  }
58
- },
83
+ }
84
+ /** Whether `key` is present (and not the server). */
85
+ has(key) {
86
+ const backend = this.#backend();
87
+ if (!backend)
88
+ return false;
89
+ return backend.getItem(this.fullKey(key)) !== null;
90
+ }
59
91
  remove(key) {
60
- if (typeof localStorage !== "undefined") {
61
- localStorage.removeItem(key);
92
+ this.#backend()?.removeItem(this.fullKey(key));
93
+ }
94
+ /** Read `key`, or compute + persist `factory()` on a miss, returning the value. */
95
+ getOrSet(key, factory) {
96
+ const existing = this.get(key);
97
+ if (existing !== null)
98
+ return existing;
99
+ const value = factory();
100
+ this.set(key, value);
101
+ return value;
102
+ }
103
+ /** Keys in this store, prefix stripped. Empty array during SSR. */
104
+ keys() {
105
+ const backend = this.#backend();
106
+ if (!backend)
107
+ return [];
108
+ const out = [];
109
+ for (let i = 0; i < backend.length; i++) {
110
+ const k = backend.key(i);
111
+ if (k === null)
112
+ continue;
113
+ if (this.#prefix === "" || k.startsWith(this.#prefix)) {
114
+ out.push(k.slice(this.#prefix.length));
115
+ }
62
116
  }
63
- },
117
+ return out;
118
+ }
119
+ /** Remove this store's keys. With no prefix this clears the whole area. */
64
120
  clear() {
65
- if (typeof localStorage !== "undefined") {
66
- localStorage.clear();
121
+ const backend = this.#backend();
122
+ if (!backend)
123
+ return;
124
+ if (this.#prefix === "") {
125
+ backend.clear();
126
+ return;
127
+ }
128
+ for (const key of this.keys())
129
+ backend.removeItem(this.fullKey(key));
130
+ }
131
+ }
132
+ /**
133
+ * Default `localStorage` store (no namespace). Backward-compatible with the
134
+ * previous `storage.get/set/remove/clear` helper, plus `has`/`keys`/`getOrSet`.
135
+ */
136
+ export const storage = new WebStorage();
137
+ /** Default `sessionStorage` store (no namespace) — cleared when the tab closes. */
138
+ export const session = new WebStorage({ area: "session" });
139
+ /**
140
+ * A {@link Signal} whose value is mirrored to web storage.
141
+ *
142
+ * The initial value is read from storage (falling back to `initial` on a miss)
143
+ * and every write is persisted via a reactive `effect`. With `crossTab`
144
+ * (default `true`, local area only) the signal also updates live when another
145
+ * tab writes the same key. SSR-safe: with no `window` it is a plain in-memory
146
+ * signal seeded with `initial`. Because the mirror runs inside `effect`, a
147
+ * `persistedSignal` created in a component's setup is disposed with it.
148
+ */
149
+ export function persistedSignal(key, initial, options = {}) {
150
+ const store = new WebStorage(options);
151
+ const stored = store.get(key);
152
+ const sig = signal(stored !== null ? stored : initial);
153
+ // Mirror every change back to storage; runs once immediately, then on change.
154
+ effect(() => {
155
+ store.set(key, sig());
156
+ });
157
+ if ((options.crossTab ?? true) &&
158
+ (options.area ?? "local") === "local" &&
159
+ typeof window !== "undefined") {
160
+ const fullKey = store.fullKey(key);
161
+ window.addEventListener("storage", (event) => {
162
+ if (event.key !== fullKey || event.newValue === null)
163
+ return;
164
+ try {
165
+ sig(JSON.parse(event.newValue));
166
+ }
167
+ catch {
168
+ // Ignore a malformed cross-tab write.
169
+ }
170
+ });
171
+ }
172
+ return sig;
173
+ }
174
+ // ─── Reactive browser-state signals ──────────────────────────────────
175
+ //
176
+ // Each returns a Signal seeded from the current browser state and refreshed
177
+ // when the relevant event fires. The listener lives for the page lifetime, so
178
+ // create these ONCE at module scope and share the signal rather than calling
179
+ // them per component render. SSR-safe: with no `window`/`document`/`navigator`
180
+ // they return a plain signal seeded with a sensible default and never listen.
181
+ /**
182
+ * Internal: a signal seeded by `read()` and refreshed when any of `events`
183
+ * fire on `target`. `target` is `undefined` during SSR — then it's a static
184
+ * signal of the default `read()`.
185
+ */
186
+ function eventSignal(target, events, read) {
187
+ const sig = signal(read());
188
+ if (target) {
189
+ const update = () => {
190
+ sig(read());
191
+ };
192
+ for (const ev of events)
193
+ target.addEventListener(ev, update);
194
+ }
195
+ return sig;
196
+ }
197
+ /** Reactive `window.matchMedia(query).matches`. `false` during SSR. */
198
+ export function mediaQuery(query) {
199
+ if (typeof window === "undefined" ||
200
+ typeof window.matchMedia !== "function") {
201
+ return signal(false);
202
+ }
203
+ const mql = window.matchMedia(query);
204
+ const sig = signal(mql.matches);
205
+ mql.addEventListener("change", (event) => {
206
+ sig(event.matches);
207
+ });
208
+ return sig;
209
+ }
210
+ /** Reactive `prefers-color-scheme: dark`. Shortcut over {@link mediaQuery}. */
211
+ export function prefersDark() {
212
+ return mediaQuery("(prefers-color-scheme: dark)");
213
+ }
214
+ /** Reactive online/offline status (`navigator.onLine`). `true` during SSR. */
215
+ export function online() {
216
+ const read = () => typeof navigator === "undefined" ? true : navigator.onLine;
217
+ return eventSignal(typeof window === "undefined" ? undefined : window, ["online", "offline"], read);
218
+ }
219
+ /** Reactive `{ width, height }` of the viewport. `{0,0}` during SSR. */
220
+ export function windowSize() {
221
+ const read = () => typeof window === "undefined"
222
+ ? { width: 0, height: 0 }
223
+ : { width: window.innerWidth, height: window.innerHeight };
224
+ return eventSignal(typeof window === "undefined" ? undefined : window, ["resize"], read);
225
+ }
226
+ /** Reactive tab visibility (`!document.hidden`). `true` during SSR. */
227
+ export function visibility() {
228
+ const read = () => typeof document === "undefined" ? true : !document.hidden;
229
+ return eventSignal(typeof document === "undefined" ? undefined : document, ["visibilitychange"], read);
230
+ }
231
+ /** Reactive `window.location.hash`. `""` during SSR. */
232
+ export function hash() {
233
+ const read = () => typeof window === "undefined" ? "" : window.location.hash;
234
+ return eventSignal(typeof window === "undefined" ? undefined : window, ["hashchange"], read);
235
+ }
236
+ // ─── URL / history (SPA navigation) ──────────────────────────────────
237
+ /** Go back one history entry. No-op during SSR. */
238
+ export function back() {
239
+ if (typeof window !== "undefined")
240
+ window.history.back();
241
+ }
242
+ /** Go forward one history entry. No-op during SSR. */
243
+ export function forward() {
244
+ if (typeof window !== "undefined")
245
+ window.history.forward();
246
+ }
247
+ /**
248
+ * SPA navigation: push `url` onto history WITHOUT a full page reload (contrast
249
+ * {@link redirect}, which reloads). Emits a `popstate` event so reactive URL
250
+ * consumers — e.g. {@link queryParam} or a router — pick up the change. No-op
251
+ * during SSR.
252
+ */
253
+ export function navigate(url) {
254
+ if (typeof window === "undefined")
255
+ return;
256
+ window.history.pushState({}, "", url);
257
+ window.dispatchEvent(new Event("popstate"));
258
+ }
259
+ /**
260
+ * A {@link Signal} bound to a single URL query parameter. Reading reflects the
261
+ * current value (`null` when absent); writing updates the URL via `pushState`
262
+ * (no reload). Stays in sync with back/forward and {@link navigate} through the
263
+ * `popstate` event. SSR-safe: a plain `null` signal with no listeners.
264
+ */
265
+ export function queryParam(key) {
266
+ const read = () => typeof window === "undefined"
267
+ ? null
268
+ : new URLSearchParams(window.location.search).get(key);
269
+ const sig = signal(read());
270
+ if (typeof window !== "undefined") {
271
+ window.addEventListener("popstate", () => {
272
+ sig(read());
273
+ });
274
+ // Mirror writes back to the URL (skips the no-op initial run).
275
+ effect(() => {
276
+ const value = sig();
277
+ const url = new URL(window.location.href);
278
+ if (value === null)
279
+ url.searchParams.delete(key);
280
+ else
281
+ url.searchParams.set(key, value);
282
+ const next = url.pathname + url.search + url.hash;
283
+ const current = window.location.pathname +
284
+ window.location.search +
285
+ window.location.hash;
286
+ if (next !== current)
287
+ window.history.pushState({}, "", next);
288
+ });
289
+ }
290
+ return sig;
291
+ }
292
+ /**
293
+ * SSR-safe cookie accessor — the one store {@link WebStorage} can't cover, for
294
+ * values the server also reads. Reads return `null` during SSR; writes are a
295
+ * no-op. Names and values are URL-encoded.
296
+ */
297
+ export const cookie = {
298
+ get(name) {
299
+ if (typeof document === "undefined")
300
+ return null;
301
+ const prefix = `${encodeURIComponent(name)}=`;
302
+ for (const part of document.cookie.split("; ")) {
303
+ if (part.startsWith(prefix)) {
304
+ return decodeURIComponent(part.slice(prefix.length));
305
+ }
306
+ }
307
+ return null;
308
+ },
309
+ set(name, value, options = {}) {
310
+ if (typeof document === "undefined")
311
+ return;
312
+ const parts = [
313
+ `${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
314
+ `path=${options.path ?? "/"}`,
315
+ ];
316
+ if (options.maxAge !== undefined)
317
+ parts.push(`max-age=${options.maxAge}`);
318
+ if (options.expires)
319
+ parts.push(`expires=${options.expires.toUTCString()}`);
320
+ if (options.domain)
321
+ parts.push(`domain=${options.domain}`);
322
+ if (options.sameSite)
323
+ parts.push(`samesite=${options.sameSite}`);
324
+ if (options.secure)
325
+ parts.push("secure");
326
+ // biome-ignore lint/suspicious/noDocumentCookie: the Cookie Store API is async-only and unsupported in Safari/Firefox; `document.cookie` is the sole sync, broadly-supported write path for an SSR-safe helper.
327
+ document.cookie = parts.join("; ");
328
+ },
329
+ remove(name, options = {}) {
330
+ this.set(name, "", { ...options, maxAge: 0, expires: new Date(0) });
331
+ },
332
+ };
333
+ // ─── Clipboard & Web Share ───────────────────────────────────────────
334
+ /** Async clipboard access. Methods return `false`/`null` when unavailable. */
335
+ export const clipboard = {
336
+ /** Copy `text`. Returns whether it succeeded. */
337
+ async copy(text) {
338
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
339
+ return false;
340
+ }
341
+ try {
342
+ await navigator.clipboard.writeText(text);
343
+ return true;
344
+ }
345
+ catch {
346
+ return false;
347
+ }
348
+ },
349
+ /** Read clipboard text, or `null` if unavailable / denied. */
350
+ async read() {
351
+ if (typeof navigator === "undefined" || !navigator.clipboard?.readText) {
352
+ return null;
353
+ }
354
+ try {
355
+ return await navigator.clipboard.readText();
356
+ }
357
+ catch {
358
+ return null;
67
359
  }
68
360
  },
69
361
  };
362
+ /**
363
+ * Invoke the native Web Share sheet. Returns `false` when unsupported or the
364
+ * user cancels — never throws.
365
+ */
366
+ export async function share(data) {
367
+ if (typeof navigator === "undefined" ||
368
+ typeof navigator.share !== "function") {
369
+ return false;
370
+ }
371
+ try {
372
+ await navigator.share(data);
373
+ return true;
374
+ }
375
+ catch {
376
+ return false;
377
+ }
378
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `HttpClient` — a small typed wrapper over `fetch` so call sites read
3
+ * `await http.get<User>("/auth/me")` instead of hand-rolling headers,
4
+ * `res.json()`, and status checks.
5
+ *
6
+ * - Auto JSON: a plain-object/array body is `JSON.stringify`-d with a
7
+ * `Content-Type: application/json` header; a JSON response is parsed.
8
+ * `FormData`/`Blob`/`URLSearchParams`/`string`/binary bodies pass through
9
+ * untouched.
10
+ * - Bearer auth: a `token` (string or getter, read fresh per request) is sent
11
+ * as `Authorization: Bearer …` unless the caller set the header themselves.
12
+ * - Errors: a non-2xx response rejects with an {@link HttpError} carrying the
13
+ * status, the `Response`, and the parsed body.
14
+ *
15
+ * Node-free and isomorphic — uses the global `fetch` (browsers, Node 18+,
16
+ * Workers, Bun, Deno). Part of the client barrel.
17
+ */
18
+ export interface HttpClientOptions {
19
+ /** Prepended to every request URL, unless the URL is already absolute. */
20
+ baseURL?: string;
21
+ /** Headers merged into every request. */
22
+ headers?: Record<string, string>;
23
+ /**
24
+ * Bearer token sent as `Authorization: Bearer <token>`. A getter is read
25
+ * fresh on each request (so a rotated/late-set token is always current);
26
+ * a `null`/`undefined` result omits the header.
27
+ */
28
+ token?: string | null | (() => string | null | undefined);
29
+ /** Default `credentials` mode (e.g. `"include"` to send cookies). */
30
+ credentials?: RequestCredentials;
31
+ }
32
+ export interface HttpRequestOptions<T = unknown> {
33
+ /** Query params appended to the URL. `null`/`undefined` values are skipped. */
34
+ query?: Record<string, string | number | boolean | null | undefined>;
35
+ /** Extra headers for this request (override the client defaults). */
36
+ headers?: Record<string, string>;
37
+ /** Per-request bearer token override (`null` to force-omit). */
38
+ token?: string | null;
39
+ /** Abort signal. */
40
+ signal?: AbortSignal;
41
+ /** `credentials` mode for this request. */
42
+ credentials?: RequestCredentials;
43
+ /**
44
+ * Runtime validator/mapper for the parsed body. When provided, the return
45
+ * type is whatever it returns — no unchecked cast. When omitted, the parsed
46
+ * body is returned as `T` (an UNCHECKED assertion of the response shape).
47
+ */
48
+ parse?: (raw: unknown) => T;
49
+ }
50
+ /** Thrown on a non-2xx response. Carries the status, the `Response`, and the parsed body. */
51
+ export declare class HttpError extends Error {
52
+ readonly status: number;
53
+ readonly response: Response;
54
+ readonly data: unknown;
55
+ constructor(response: Response, data: unknown);
56
+ }
57
+ export declare class HttpClient {
58
+ #private;
59
+ constructor(options?: HttpClientOptions);
60
+ /** Set a default header for every subsequent request (case-insensitive replace). Chainable. */
61
+ setHeader(name: string, value: string): this;
62
+ /** Merge several default headers at once. Chainable. */
63
+ setHeaders(headers: Record<string, string>): this;
64
+ /** Remove a default header (case-insensitive). Chainable. */
65
+ removeHeader(name: string): this;
66
+ /** A copy of the current default headers. */
67
+ getHeaders(): Record<string, string>;
68
+ get<T>(url: string, options?: HttpRequestOptions<T>): Promise<T>;
69
+ delete<T>(url: string, options?: HttpRequestOptions<T>): Promise<T>;
70
+ post<T>(url: string, body?: unknown, options?: HttpRequestOptions<T>): Promise<T>;
71
+ put<T>(url: string, body?: unknown, options?: HttpRequestOptions<T>): Promise<T>;
72
+ patch<T>(url: string, body?: unknown, options?: HttpRequestOptions<T>): Promise<T>;
73
+ /** Send a request and return the raw `Response` (no parsing, no throw on non-2xx). */
74
+ raw(method: string, url: string, body?: unknown, options?: HttpRequestOptions): Promise<Response>;
75
+ /** Derive a new client with merged defaults (e.g. a scope that adds a token). */
76
+ extend(options: HttpClientOptions): HttpClient;
77
+ }
78
+ /** Default same-origin client. Configure your own via `new HttpClient({ … })`. */
79
+ export declare const http: HttpClient;