UNPKG

@tantainnovative/ndpr-toolkit

Version:

Nigeria Data Protection Toolkit — enterprise-grade compliance components for the Nigeria Data Protection Act (NDPA) 2023

255 lines (241 loc) 9.57 kB
/** * Production-ready API storage adapter. * * Backward-compatible with the 3.5.x signature — `apiAdapter('/api/x')` * still works exactly as before. New options are all opt-in. * * @example basic * const adapter = apiAdapter<ConsentSettings>('/api/consent'); * * @example with credentials and CSRF * const adapter = apiAdapter<ConsentSettings>('/api/consent', { * credentials: 'include', * headers: () => ({ * 'X-CSRF-Token': document.querySelector<HTMLMetaElement>( * 'meta[name="csrf-token"]' * )?.content ?? '', * }), * }); * * @example with retry + telemetry * const adapter = apiAdapter<ConsentSettings>('/api/consent', { * retry: { attempts: 2, baseDelayMs: 300 }, * onError: (ctx) => Sentry.captureException(ctx.error, { extra: ctx }), * onSuccess: (ctx) => analytics.track('consent_saved', { method: ctx.method }), * }); * * @example with response unwrap * const adapter = apiAdapter<ConsentSettings>('/api/consent', { * // API returns { data: ConsentSettings, ok: true } * unwrap: (raw) => (raw as { data: ConsentSettings }).data, * }); */ export declare function apiAdapter<T = unknown>(endpoint: string, options?: ApiAdapterOptions<T>): StorageAdapter<T>; export declare interface ApiAdapterErrorContext<T = unknown> { /** Which adapter operation triggered this — `load`, `save`, or `remove`. */ method: ApiAdapterMethod; /** The endpoint URL that failed. */ endpoint: string; /** Underlying error (for network failures / parse errors). */ error?: unknown; /** Response object, if a response was received. */ response?: Response; /** HTTP status code, if available. */ status?: number; /** For `save`, the payload that failed to send. */ payload?: T; /** Which retry attempt this is (0 = first try). Capped at `retry.attempts`. */ attempt: number; } export declare type ApiAdapterMethod = 'load' | 'save' | 'remove'; export declare interface ApiAdapterOptions<T = unknown> { /** * Extra HTTP headers to send with every request. Useful for `Authorization`, * `X-CSRF-Token`, `X-Requested-With`, etc. * * Can also be a function that returns headers, which lets you read a CSRF * token from the DOM/cookie at request time rather than at adapter * construction time. */ headers?: Record<string, string> | (() => Record<string, string>); /** * Forwarded to fetch's `credentials` option. Defaults to `'same-origin'` * (the browser default). Set to `'include'` for cross-origin endpoints * that need cookies / auth. */ credentials?: RequestCredentials; /** * HTTP method override for the load operation. Defaults to `'GET'`. */ loadMethod?: 'GET' | 'POST'; /** * HTTP method override for the save operation. Defaults to `'POST'`. Some * REST APIs prefer `'PUT'` for upsert semantics. */ saveMethod?: 'POST' | 'PUT' | 'PATCH'; /** * Transform the raw JSON response into the expected `T`. Useful for APIs * that wrap responses in `{ data: ... }` or similar envelopes. Called * after `res.json()`. If omitted, the parsed JSON is used as-is. */ unwrap?: (raw: unknown) => T | null; /** * Retry policy for failed requests. Defaults to no retries (preserves the * pre-3.6.0 behaviour). When configured, applies to all three operations. */ retry?: ApiAdapterRetryConfig; /** * Called when a request fails (after all retries exhausted). The adapter * still returns a graceful null/void result so the consuming hook * doesn't crash — this hook is for telemetry, toasts, or audit logging. */ onError?: (ctx: ApiAdapterErrorContext<T>) => void; /** * Called when a request succeeds. Useful for cache invalidation, * analytics, or syncing other state. */ onSuccess?: (ctx: ApiAdapterSuccessContext<T>) => void; /** * Per-request fetch options to merge into every request. Use this for * things `fetch` itself supports that aren't directly modelled above — * `signal`, `mode`, `cache`, `redirect`, etc. */ fetchInit?: Omit<RequestInit, 'method' | 'headers' | 'body' | 'credentials'>; } export declare interface ApiAdapterRetryConfig { /** * Number of additional attempts after the initial request. Defaults to 0 * (no retries). e.g. `attempts: 2` means up to 3 total requests. */ attempts?: number; /** * Base delay in ms between attempts. Defaults to 250ms. The actual delay * uses exponential backoff: `baseDelayMs * 2^attempt`. */ baseDelayMs?: number; /** * Predicate that decides whether to retry given the failure context. By * default we retry on network errors and 5xx responses, but not on 4xx * (those are client errors that won't fix themselves). */ shouldRetry?: (ctx: ApiAdapterErrorContext<unknown>) => boolean; } export declare interface ApiAdapterSuccessContext<T = unknown> { /** Which adapter operation succeeded — `load`, `save`, or `remove`. */ method: ApiAdapterMethod; /** The endpoint URL. */ endpoint: string; /** Response object. */ response: Response; /** For `load` operations, the parsed (and optionally unwrapped) data. */ data?: T; /** For `save` operations, the payload that was sent. */ payload?: T; } /** * Compose a primary adapter with one or more secondary adapters. Reads * always go to the primary; writes (`save` / `remove`) fan out to the * primary first, then each secondary. Secondary failures are logged but * never propagated. * * Useful for shadowing localStorage to an API endpoint, mirroring consent * to a cookie for SSR, or building offline-first sync. * * @example * ```ts * import { * composeAdapters, * localStorageAdapter, * apiAdapter, * } from '@tantainnovative/ndpr-toolkit/adapters'; * import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks'; * * const adapter = composeAdapters( * localStorageAdapter('ndpr_consent'), * apiAdapter('/api/consent'), * ); * useConsent({ options, adapter }); * ``` */ export declare function composeAdapters<T = unknown>(primary: StorageAdapter<T>, ...secondaries: StorageAdapter<T>[]): StorageAdapter<T>; /** * Storage adapter backed by a browser cookie. Useful for consent state that * needs to be shared with server-side rendering, or for cross-subdomain * persistence via the `domain` option. * * @example * ```ts * import { cookieAdapter } from '@tantainnovative/ndpr-toolkit/adapters'; * import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks'; * * const adapter = cookieAdapter('ndpr_consent', { * domain: '.example.com', * sameSite: 'Lax', * expires: 180, * }); * useConsent({ options, adapter }); * ``` */ export declare function cookieAdapter<T = unknown>(key: string, options?: CookieAdapterOptions): StorageAdapter<T>; export declare interface CookieAdapterOptions { domain?: string; path?: string; expires?: number; secure?: boolean; sameSite?: 'Strict' | 'Lax' | 'None'; } /** * Storage adapter backed by `window.localStorage`. The default adapter used * by every hook in the toolkit when no `adapter` prop is supplied. * * Safe to import server-side — every method short-circuits when * `window` is undefined, so calling `load()` on the server returns `null`. * * @example * ```ts * import { localStorageAdapter } from '@tantainnovative/ndpr-toolkit/adapters'; * import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks'; * * const adapter = localStorageAdapter('ndpr_consent'); * useConsent({ options, adapter }); * ``` */ export declare function localStorageAdapter<T = unknown>(key: string): StorageAdapter<T>; /** * Storage adapter backed by an in-memory value. Useful in tests, Storybook, * SSR previews, or anywhere persistence across reloads is undesirable. * * @example * ```ts * import { memoryAdapter } from '@tantainnovative/ndpr-toolkit/adapters'; * import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks'; * * const adapter = memoryAdapter({ consents: {}, version: '1.0' }); * useConsent({ options, adapter }); * ``` */ export declare function memoryAdapter<T = unknown>(initialData?: T): StorageAdapter<T>; /** * Storage adapter backed by `window.sessionStorage`. Data is scoped to the * current tab and discarded when the tab closes — useful for consent * choices that should not survive a fresh session. * * @example * ```ts * import { sessionStorageAdapter } from '@tantainnovative/ndpr-toolkit/adapters'; * import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks'; * * const adapter = sessionStorageAdapter('ndpr_consent'); * useConsent({ options, adapter }); * ``` */ export declare function sessionStorageAdapter<T = unknown>(key: string): StorageAdapter<T>; export declare interface StorageAdapter<T = unknown> { /** Load persisted data. Called once on hook mount. */ load(): T | null | Promise<T | null>; /** Persist data. Called on every state change. */ save(data: T): void | Promise<void>; /** Clear persisted data. Called on reset. */ remove(): void | Promise<void>; } export { }