@classytic/arc-next 0.4.0 → 0.5.0

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/dist/cache.js ADDED
@@ -0,0 +1,197 @@
1
+ //#region src/cache.ts
2
+ const DEFAULT_QUERY_CONFIG = {
3
+ staleTime: 300 * 1e3,
4
+ gcTime: 1800 * 1e3,
5
+ refetchOnWindowFocus: false,
6
+ retry: 0
7
+ };
8
+ /** Pre-built query config presets for common data freshness patterns. */
9
+ const QUERY_CONFIGS = {
10
+ realtime: {
11
+ staleTime: 2e4,
12
+ refetchInterval: 3e4
13
+ },
14
+ frequent: { staleTime: 6e4 },
15
+ stable: { staleTime: 3e5 },
16
+ static: { staleTime: 6e5 }
17
+ };
18
+ /** Well-known keys checked in order for list responses. */
19
+ const LIST_KEYS = [
20
+ "docs",
21
+ "data",
22
+ "items",
23
+ "results"
24
+ ];
25
+ /** Well-known keys checked in order for detail responses. */
26
+ const DETAIL_KEYS = [
27
+ "data",
28
+ "doc",
29
+ "item",
30
+ "result"
31
+ ];
32
+ /**
33
+ * Extract `_id` or `id` from any item. Returns `null` if neither exists.
34
+ * Coerces numeric IDs to strings so cache keys stay consistent.
35
+ */
36
+ function getItemId(item) {
37
+ if (!item || typeof item !== "object") return null;
38
+ const obj = item;
39
+ const id = obj._id ?? obj.id;
40
+ return typeof id === "string" ? id : id ? String(id) : null;
41
+ }
42
+ /**
43
+ * Normalize any pagination response shape (offset / keyset / aggregate) to a
44
+ * uniform `PaginationData` object. Returns `null` when no pagination signal
45
+ * is present.
46
+ */
47
+ function normalizePagination(data) {
48
+ if (!data || typeof data !== "object") return null;
49
+ const d = data;
50
+ const method = d.method ?? null;
51
+ const isKeyset = method === "keyset" || d.hasMore != null && d.total == null && d.pages == null;
52
+ const hasTotal = d.total != null || d.totalDocs != null;
53
+ const hasPages = d.pages != null || d.totalPages != null;
54
+ if (!hasTotal && !hasPages && !isKeyset) return null;
55
+ return {
56
+ method,
57
+ total: Number(d.total ?? d.totalDocs ?? 0),
58
+ pages: Number(d.pages ?? d.totalPages ?? (isKeyset ? 0 : 1)),
59
+ page: Number(d.page ?? d.currentPage ?? (isKeyset ? 0 : 1)),
60
+ limit: Number(d.limit ?? 10),
61
+ hasNext: Boolean(d.hasNext ?? d.hasNextPage ?? d.hasMore ?? false),
62
+ hasPrev: Boolean(d.hasPrev ?? d.hasPrevPage ?? false),
63
+ ...isKeyset ? { next: d.next ?? null } : {}
64
+ };
65
+ }
66
+ /**
67
+ * Permissive list extractor. Checks well-known keys (`docs`, `data`, `items`,
68
+ * `results`) then falls back to *any* top-level array — so `{ products: [...] }`
69
+ * and `{ users: [...] }` work without per-resource configuration.
70
+ */
71
+ function extractItems(data) {
72
+ if (!data) return [];
73
+ if (Array.isArray(data)) return data;
74
+ if (typeof data !== "object") return [];
75
+ const d = data;
76
+ for (const key of LIST_KEYS) if (Array.isArray(d[key])) return d[key];
77
+ for (const value of Object.values(d)) if (Array.isArray(value)) return value;
78
+ return [];
79
+ }
80
+ /**
81
+ * Strict detail extractor. Checks well-known keys (`data`, `doc`, `item`,
82
+ * `result`); falls back to returning the response as-is. Primitive responses
83
+ * (string/number/boolean) pass through.
84
+ */
85
+ function extractItem(data) {
86
+ if (data == null) return null;
87
+ if (typeof data !== "object") return data;
88
+ const d = data;
89
+ for (const key of DETAIL_KEYS) if (d[key] != null) return d[key];
90
+ return d;
91
+ }
92
+ /**
93
+ * Optimistic-update helper that mutates the items array of a list cache
94
+ * regardless of which key holds it. Auto-adjusts `total`/`totalDocs` when
95
+ * the array length changes.
96
+ */
97
+ function updateListCache(listData, updater) {
98
+ if (!listData) return listData;
99
+ if (Array.isArray(listData)) return updater(listData);
100
+ if (typeof listData !== "object") return listData;
101
+ const d = listData;
102
+ let arrayField = null;
103
+ for (const key of LIST_KEYS) if (Array.isArray(d[key])) {
104
+ arrayField = key;
105
+ break;
106
+ }
107
+ if (!arrayField) {
108
+ for (const [key, value] of Object.entries(d)) if (Array.isArray(value)) {
109
+ arrayField = key;
110
+ break;
111
+ }
112
+ }
113
+ if (!arrayField) return listData;
114
+ const updated = updater(d[arrayField]);
115
+ const original = d[arrayField];
116
+ const delta = updated.length - original.length;
117
+ const result = {
118
+ ...d,
119
+ [arrayField]: updated
120
+ };
121
+ if (delta !== 0) {
122
+ if (d.total != null) result.total = Math.max(0, Number(d.total) + delta);
123
+ if (d.totalDocs != null) result.totalDocs = Math.max(0, Number(d.totalDocs) + delta);
124
+ }
125
+ return result;
126
+ }
127
+ /**
128
+ * Build a hierarchical query-key factory for a resource. The returned shape
129
+ * is identical between server (prefetch) and client (hooks), so RSC SSR
130
+ * hydration matches what client-side `useList`/`useDetail` produce.
131
+ */
132
+ function createQueryKeys(entityKey) {
133
+ return {
134
+ all: [entityKey],
135
+ lists: () => [entityKey, "list"],
136
+ list: (params) => [
137
+ entityKey,
138
+ "list",
139
+ params
140
+ ],
141
+ details: () => [entityKey, "detail"],
142
+ detail: (id) => [
143
+ entityKey,
144
+ "detail",
145
+ id
146
+ ],
147
+ scopedDetail: (id, organizationId) => organizationId ? [
148
+ entityKey,
149
+ "detail",
150
+ id,
151
+ { _org: organizationId }
152
+ ] : [
153
+ entityKey,
154
+ "detail",
155
+ id
156
+ ],
157
+ custom: (key, ...args) => [
158
+ entityKey,
159
+ key,
160
+ ...args
161
+ ],
162
+ scopedList: (scope, params) => [
163
+ entityKey,
164
+ "list",
165
+ {
166
+ _scope: scope,
167
+ ...params
168
+ }
169
+ ]
170
+ };
171
+ }
172
+ /**
173
+ * Build cache read/write/invalidate helpers bound to the given key factory.
174
+ * Server-safe — operates on a `QueryClient` instance which can be a per-request
175
+ * server client (during prefetch) or the browser singleton.
176
+ */
177
+ function createCacheUtils(KEYS) {
178
+ return {
179
+ invalidateAll: (client) => client.invalidateQueries({ queryKey: KEYS.all }),
180
+ invalidateLists: (client) => client.invalidateQueries({ queryKey: KEYS.lists() }),
181
+ invalidateDetail: (client, id) => client.invalidateQueries({ queryKey: KEYS.detail(id) }),
182
+ setDetail: (client, id, data) => client.setQueryData(KEYS.detail(id), { data }),
183
+ getDetail: (client, id) => {
184
+ return client.getQueryData(KEYS.detail(id))?.data;
185
+ },
186
+ removeDetail: (client, id) => client.removeQueries({ queryKey: KEYS.detail(id) }),
187
+ invalidateScopedDetail: (client, id, organizationId) => client.invalidateQueries({ queryKey: KEYS.scopedDetail(id, organizationId) }),
188
+ setScopedDetail: (client, id, organizationId, data) => client.setQueryData(KEYS.scopedDetail(id, organizationId), { data }),
189
+ getScopedDetail: (client, id, organizationId) => {
190
+ return client.getQueryData(KEYS.scopedDetail(id, organizationId))?.data;
191
+ },
192
+ removeScopedDetail: (client, id, organizationId) => client.removeQueries({ queryKey: KEYS.scopedDetail(id, organizationId) })
193
+ };
194
+ }
195
+
196
+ //#endregion
197
+ export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, extractItems, getItemId, normalizePagination, updateListCache };
package/dist/client.d.ts CHANGED
@@ -11,6 +11,40 @@ type UseRouterHook = () => {
11
11
  scroll?: boolean;
12
12
  }) => void;
13
13
  };
14
+ /**
15
+ * Top-level error codes arc's `errorHandlerPlugin` emits on the response root
16
+ * (`json.code`). Mirrors `statusCodeToCode()` + the special `VALIDATION_ERROR`
17
+ * / `DUPLICATE_KEY` / `INTERNAL_ERROR` branches.
18
+ *
19
+ * Open via intersection with `(string & {})` so unknown codes from custom
20
+ * `errorMappers` / `errorMap` still satisfy the type.
21
+ */
22
+ /**
23
+ * Top-level HTTP-status error codes arc's `errorHandlerPlugin` emits on
24
+ * `json.code`. Exported as a `const` array first so callers get both
25
+ * compile-time exhaustiveness AND runtime iteration (e.g. building a
26
+ * client-side i18n lookup, or whitelisting which codes get retried).
27
+ */
28
+ declare const KNOWN_TOP_LEVEL_CODES: readonly ["BAD_REQUEST", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "METHOD_NOT_ALLOWED", "CONFLICT", "UNPROCESSABLE_ENTITY", "RATE_LIMITED", "INTERNAL_ERROR", "BAD_GATEWAY", "SERVICE_UNAVAILABLE", "GATEWAY_TIMEOUT", "VALIDATION_ERROR", "DUPLICATE_KEY", "DOMAIN_ERROR"];
29
+ /**
30
+ * Top-level HTTP-status error code union derived from {@link KNOWN_TOP_LEVEL_CODES}.
31
+ * `(string & {})` keeps the type open so non-arc backends or custom error mappers
32
+ * still satisfy it.
33
+ */
34
+ type ArcTopLevelErrorCode = (typeof KNOWN_TOP_LEVEL_CODES)[number] | (string & {});
35
+ /**
36
+ * Nested business-logic codes arc's mixins / preset routes / org guards emit
37
+ * on `json.details.code`. These are distinct from the HTTP-status code above —
38
+ * `error.code` (top-level) vs `error.detailsCode` (nested). When a route
39
+ * returns 403 with `details.code: 'ORG_CONTEXT_REQUIRED'`, hosts read
40
+ * `detailsCode` to disambiguate "missing org context" from "permission denied".
41
+ */
42
+ declare const KNOWN_DETAILS_CODES: readonly ["ORG_CONTEXT_REQUIRED", "ORG_ROLE_REQUIRED", "OWNERSHIP_DENIED", "MIXED_UPDATE_SHAPE", "ALL_FIELDS_STRIPPED", "BEFORE_RESTORE_HOOK_ERROR"];
43
+ /**
44
+ * Nested business-logic error code union derived from {@link KNOWN_DETAILS_CODES}.
45
+ * Same `(string & {})` escape hatch as {@link ArcTopLevelErrorCode}.
46
+ */
47
+ type ArcDetailsErrorCode = (typeof KNOWN_DETAILS_CODES)[number] | (string & {});
14
48
  interface ArcApiErrorOptions {
15
49
  status: number;
16
50
  statusText: string;
@@ -39,13 +73,109 @@ declare class ArcApiError extends Error {
39
73
  readonly endpoint: string;
40
74
  readonly method: HttpMethod;
41
75
  constructor(message: string, options: ArcApiErrorOptions);
42
- /** Extract field-level validation errors if present (e.g. `{ field: "message" }`) */
76
+ /**
77
+ * Top-level error code from arc's response envelope (`json.code`).
78
+ *
79
+ * One of {@link ArcTopLevelErrorCode} — covers HTTP-status codes (`FORBIDDEN`,
80
+ * `CONFLICT`...) plus the dedicated `VALIDATION_ERROR` / `DUPLICATE_KEY`
81
+ * branches arc emits. Returns `null` if the response carries no envelope.
82
+ *
83
+ * @example
84
+ * if (error.code === 'DUPLICATE_KEY') showRetryAsAdmin();
85
+ */
86
+ get code(): ArcTopLevelErrorCode | null;
87
+ /**
88
+ * Nested business-logic code from arc's `json.details.code` slot.
89
+ *
90
+ * Arc's preset mixins (bulk, softDelete) and org guards emit specific reason
91
+ * codes here — distinct from the HTTP-status `code` above. The most common
92
+ * is `ORG_CONTEXT_REQUIRED` (bulk operations + orgGuard reject when the
93
+ * caller's `request.scope.organizationId` is missing).
94
+ *
95
+ * @example
96
+ * if (error.detailsCode === 'ORG_CONTEXT_REQUIRED') {
97
+ * alert('Configure auth before bulk operations: configureAuth({ getOrgId })');
98
+ * }
99
+ */
100
+ get detailsCode(): ArcDetailsErrorCode | null;
101
+ /**
102
+ * Extract field-level validation errors as `{ field: message }` map.
103
+ *
104
+ * Reads three response shapes used by Arc:
105
+ * 1. `{ errors: { email: 'invalid' } }` — record form (legacy / app-level handlers).
106
+ * 2. `{ details: { errors: [{ field, message }] } }` — Fastify AJV + Arc errorHandler emit this.
107
+ * 3. `{ details: { errors: [{ instancePath, message, params }] } }` — raw AJV passthrough.
108
+ *
109
+ * Returns null when none of the shapes match.
110
+ */
43
111
  get fieldErrors(): Record<string, string> | null;
44
112
  }
45
113
  /**
46
114
  * Type guard for ArcApiError.
47
115
  */
48
116
  declare function isArcApiError(error: unknown): error is ArcApiError;
117
+ /**
118
+ * Detects request cancellation (AbortSignal triggered) regardless of runtime.
119
+ *
120
+ * Filtering out abort errors from real failures is a common need — without it,
121
+ * unmounting a React component mid-fetch produces noisy logs and bogus error
122
+ * toasts that look like API failures. Use this predicate to skip the catch
123
+ * branch when the request was deliberately cancelled.
124
+ *
125
+ * Handles the three AbortError shapes you'll see in the wild:
126
+ * 1. Browser `DOMException { name: 'AbortError' }`
127
+ * 2. Node 18+ / undici `Error { name: 'AbortError' }` (no DOMException)
128
+ * 3. Some polyfills / older runtimes where the error has `code: 'ERR_ABORTED'`
129
+ *
130
+ * @example
131
+ * try {
132
+ * await api.getAll({ options: { signal } });
133
+ * } catch (err) {
134
+ * if (isAbortError(err)) return; // user navigated away — silence
135
+ * showToast('Failed to load: ' + err.message);
136
+ * }
137
+ */
138
+ declare function isAbortError(error: unknown): boolean;
139
+ /**
140
+ * Generic check: is this an `ArcApiError` carrying a specific top-level OR
141
+ * nested `details.code`? Matches whichever slot the code lives in — saves
142
+ * call sites from having to know whether arc emitted it at the root or under
143
+ * `details`.
144
+ *
145
+ * @example
146
+ * if (isArcErrorCode(error, 'DUPLICATE_KEY')) showRetryUI();
147
+ * if (isArcErrorCode(error, 'ORG_CONTEXT_REQUIRED')) promptOrgSelector();
148
+ */
149
+ declare function isArcErrorCode(error: unknown, code: ArcTopLevelErrorCode | ArcDetailsErrorCode): error is ArcApiError;
150
+ /**
151
+ * Specific predicate for arc's bulk-preset + orgGuard safety code.
152
+ *
153
+ * Arc's bulk endpoints (`POST/PATCH/DELETE /:resource/bulk`) reject any call
154
+ * where `request.scope.organizationId` is missing — the wire signal is
155
+ * `403 { details: { code: 'ORG_CONTEXT_REQUIRED' } }`. Hosts hitting this
156
+ * need to call `configureAuth({ getOrgId })` before retrying.
157
+ *
158
+ * @example
159
+ * try { await api.bulkCreate({ data: [...] }); }
160
+ * catch (e) {
161
+ * if (isOrgContextRequiredError(e)) {
162
+ * console.warn('Bulk requires org context. Configure: configureAuth({ getOrgId })');
163
+ * } else throw e;
164
+ * }
165
+ */
166
+ declare function isOrgContextRequiredError(error: unknown): error is ArcApiError;
167
+ /**
168
+ * Specific predicate for arc's `VALIDATION_ERROR` (Fastify AJV + Mongoose
169
+ * ValidationError). When true, `error.fieldErrors` is populated with the
170
+ * `{ field: message }` map.
171
+ */
172
+ declare function isValidationError(error: unknown): error is ArcApiError;
173
+ /**
174
+ * Specific predicate for arc's `DUPLICATE_KEY` (unique-constraint violation).
175
+ * Arc's errorHandler classifies these uniformly across MongoDB E11000,
176
+ * Postgres 23505, and Prisma P2002.
177
+ */
178
+ declare function isDuplicateKeyError(error: unknown): error is ArcApiError;
49
179
  interface ClientConfig {
50
180
  baseUrl: string;
51
181
  internalApiKey?: string;
@@ -79,7 +209,101 @@ interface ClientConfig {
79
209
  * Default: false — opt-in per-request via `idempotencyKey` option.
80
210
  */
81
211
  autoIdempotency?: boolean;
212
+ /**
213
+ * Send `x-arc-scope: platform` on every request. Triggers arc's elevated-scope
214
+ * upgrade (member → elevated) when the caller has the appropriate permission.
215
+ * Use sparingly — typically only for internal admin tooling.
216
+ * Per-request override: set `elevated: true | false` on `RequestOptions`.
217
+ */
218
+ elevated?: boolean;
219
+ /**
220
+ * Network-failure + 5xx retry policy. Off by default — TanStack Query already
221
+ * retries query reads (3× by default), but the SDK's mutation flow and any
222
+ * direct `handleApiRequest` call get nothing without this.
223
+ *
224
+ * @example
225
+ * configureClient({
226
+ * baseUrl,
227
+ * retry: { attempts: 3, backoff: 'exponential' },
228
+ * });
229
+ */
230
+ retry?: RetryConfig;
231
+ /**
232
+ * Mutate the outgoing request before fetch. Runs once per attempt (so a retry
233
+ * re-runs the interceptor — useful for refreshed tokens or rotated trace IDs).
234
+ * Return the (possibly modified) context. Async is supported.
235
+ *
236
+ * @example
237
+ * configureClient({
238
+ * baseUrl,
239
+ * beforeRequest: (req) => ({
240
+ * ...req,
241
+ * headers: { ...req.headers, 'x-correlation-id': crypto.randomUUID() },
242
+ * }),
243
+ * });
244
+ */
245
+ beforeRequest?: BeforeRequestInterceptor;
246
+ /**
247
+ * Inspect or transform the parsed response body. Receives the same shape the
248
+ * SDK is about to return; returning a new `body` replaces it. Errors are not
249
+ * forwarded here — they throw `ArcApiError` before this runs. Async is supported.
250
+ *
251
+ * @example
252
+ * configureClient({
253
+ * baseUrl,
254
+ * afterResponse: (res) => {
255
+ * console.log(`[arc] ${res.method} ${res.endpoint} ${res.status} ${res.durationMs}ms`);
256
+ * return res; // unchanged
257
+ * },
258
+ * });
259
+ */
260
+ afterResponse?: AfterResponseInterceptor;
261
+ }
262
+ interface RetryConfig {
263
+ /** Total attempts including the first. `attempts: 3` means 1 try + 2 retries. Default: 0 (off). */
264
+ attempts?: number;
265
+ /**
266
+ * Backoff strategy.
267
+ * - `'exponential'` (default): `min(300 * 2^n, 10_000)` ms
268
+ * - `'linear'`: `300 * (n + 1)` ms
269
+ * - `(attempt) => number`: custom delay in ms; receives the 0-indexed retry attempt
270
+ */
271
+ backoff?: 'exponential' | 'linear' | ((attempt: number) => number);
272
+ /**
273
+ * Whitelist of statuses to retry on, OR a predicate. Defaults to a function
274
+ * that returns true for: any non-`ArcApiError` (network failure / fetch
275
+ * TypeError) AND `ArcApiError.status >= 500 && < 600`. Never retries on
276
+ * AbortError or 4xx.
277
+ */
278
+ retryOn?: number[] | ((error: unknown) => boolean);
82
279
  }
280
+ /** Context handed to {@link ClientConfig.beforeRequest}. Mutate + return to override. */
281
+ interface BeforeRequestContext {
282
+ method: HttpMethod;
283
+ endpoint: string;
284
+ /** Headers about to be sent. Mutating this is the recommended way to inject auth/trace headers. */
285
+ headers: Record<string, string>;
286
+ /** Body as the SDK is about to send it (already JSON-stringified, FormData, or undefined). */
287
+ body: BodyInit | undefined;
288
+ /** AbortSignal forwarded to fetch, if any. */
289
+ signal?: AbortSignal;
290
+ /** 0 on first attempt, 1+ on retries — useful for trace stamping. */
291
+ attempt: number;
292
+ }
293
+ type BeforeRequestInterceptor = (ctx: BeforeRequestContext) => BeforeRequestContext | Promise<BeforeRequestContext>;
294
+ /** Context handed to {@link ClientConfig.afterResponse}. */
295
+ interface AfterResponseContext<T = unknown> {
296
+ method: HttpMethod;
297
+ endpoint: string;
298
+ status: number;
299
+ /** Parsed response body — JSON object, blob wrapper, or text wrapper depending on Content-Type. */
300
+ body: T;
301
+ /** Total elapsed milliseconds from `beforeRequest` invocation to response parse complete. */
302
+ durationMs: number;
303
+ /** Original Response — clone before reading body if you need raw access. */
304
+ response: Response;
305
+ }
306
+ type AfterResponseInterceptor = <T = unknown>(ctx: AfterResponseContext<T>) => AfterResponseContext<T> | Promise<AfterResponseContext<T>>;
83
307
  /**
84
308
  * Configure the API client. Call once at app init before any API requests.
85
309
  *
@@ -99,9 +323,21 @@ declare function configureClient(config: ClientConfig): void;
99
323
  * Get the configured auth mode. Returns 'bearer' if not configured.
100
324
  */
101
325
  declare function getAuthMode(): 'bearer' | 'cookie' | 'header';
326
+ /** Get the configured base URL. Returns empty string if not configured. */
327
+ declare function getBaseUrl(): string;
102
328
  /** Whether auto-idempotency is enabled on the global client. */
103
329
  declare function isAutoIdempotency(): boolean;
104
330
  interface AuthConfig {
331
+ /**
332
+ * Returns the current bearer/API token, or `null` if not authenticated.
333
+ *
334
+ * **MUST resolve synchronously.** The signature is `() => string | null`, never
335
+ * `Promise<string | null>`. If your auth library exposes an async session getter
336
+ * (Better Auth, NextAuth, Clerk, OAuth flows), refresh the token out-of-band
337
+ * (timer, event listener, lazy 401 retry) and have `getToken` return the *cached*
338
+ * value. Returning a Promise will be detected and warned about in dev — but the
339
+ * underlying token will be silently treated as `null`, causing 401s.
340
+ */
105
341
  getToken?: () => string | null;
106
342
  getOrgId?: () => string | null;
107
343
  /** Custom auth header name. Used when authMode is 'header'. Default: 'x-api-key' */
@@ -113,11 +349,15 @@ interface AuthConfig {
113
349
  *
114
350
  * **SSR safety:** This sets module-level state. Call only in client-side code.
115
351
  *
352
+ * **Token resolution is synchronous.** `getToken` must return `string | null`
353
+ * synchronously — never a Promise. See {@link AuthConfig.getToken} for guidance
354
+ * on bridging async auth libraries via cached values.
355
+ *
116
356
  * @example
117
357
  * // Cookie auth (no token needed)
118
358
  * configureAuth({ getOrgId: () => currentOrg.id });
119
359
  *
120
- * // Bearer auth
360
+ * // Bearer auth — token is cached synchronously by the auth library
121
361
  * configureAuth({
122
362
  * getToken: () => session?.accessToken ?? null,
123
363
  * getOrgId: () => currentOrg?.id ?? null,
@@ -131,7 +371,34 @@ declare function getAuthContext(): {
131
371
  token: string | null;
132
372
  organizationId: string | null;
133
373
  };
374
+ /** @internal — exposed for tests; resets the dev-warn dedup flag. */
375
+ declare function _resetAuthWarnings(): void;
376
+ /** Protocol family the URL should target. `http` keeps `getBaseUrl()` as-is; `ws` rewrites `http(s)://` → `ws(s)://`. */
377
+ type StreamUrlProtocol = 'http' | 'ws';
378
+ /**
379
+ * Build an auth-aware URL using the global client + auth singletons.
380
+ *
381
+ * Single source of truth for {@link import('./sse.js').buildSseUrl} (HTTP) and
382
+ * {@link import('./ws.js').buildWsUrl} (WebSocket). Both delegate here so the
383
+ * auth-injection rule (org always, token only when `authMode !== 'cookie'`)
384
+ * stays in one place and can't drift.
385
+ *
386
+ * @param path Path appended to the base URL (leading slash recommended).
387
+ * @param params Caller-supplied params; merged with auth params. Caller wins on key collision.
388
+ * @param protocol `'http'` (default) or `'ws'` — controls the protocol rewrite.
389
+ */
390
+ declare function buildStreamUrl(path: string, params?: Record<string, string | number | boolean | null | undefined>, protocol?: StreamUrlProtocol): string;
134
391
  type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
392
+ /** Returned by `handleApiRequest` for PDF, image, and CSV responses. */
393
+ interface BlobResponse {
394
+ data: Blob;
395
+ response: Response;
396
+ }
397
+ /** Returned by `handleApiRequest` for text/plain and text/html responses. */
398
+ interface TextResponse {
399
+ data: string;
400
+ response: Response;
401
+ }
135
402
  interface ApiRequestOptions {
136
403
  body?: unknown;
137
404
  token?: string | null;
@@ -143,31 +410,96 @@ interface ApiRequestOptions {
143
410
  signal?: AbortSignal;
144
411
  /** Explicit idempotency key for this request. Sent as `Idempotency-Key` header. */
145
412
  idempotencyKey?: string;
413
+ /**
414
+ * Send `x-arc-scope: platform` for this request only — overrides `ClientConfig.elevated`.
415
+ * Pass `false` to suppress when client-level elevation is on.
416
+ */
417
+ elevated?: boolean;
146
418
  }
147
419
  interface ArcClientConfig extends ClientConfig {
148
420
  toast?: ToastHandler;
149
421
  navigation?: UseRouterHook;
422
+ /** Per-client token provider. Overrides global configureAuth().getToken. */
423
+ getToken?: () => string | null;
424
+ /** Per-client org ID provider. Overrides global configureAuth().getOrgId. */
425
+ getOrgId?: () => string | null;
426
+ /** Per-client custom auth header name. Used when authMode is 'header'. */
427
+ headerName?: string;
150
428
  }
151
429
  interface ArcClient {
152
430
  request: <T = unknown>(method: HttpMethod, endpoint: string, options?: ApiRequestOptions) => Promise<T>;
153
431
  config: ClientConfig;
154
432
  toast?: ToastHandler;
155
433
  navigation?: UseRouterHook;
434
+ /** Per-client auth context. Falls back to global configureAuth() when not set. */
435
+ auth?: {
436
+ getToken?: () => string | null;
437
+ getOrgId?: () => string | null;
438
+ headerName?: string;
439
+ };
156
440
  }
157
441
  /**
158
442
  * Create an isolated API client for a specific backend.
159
- * Use this when your app needs to talk to multiple APIs.
443
+ * Use this when your app needs to talk to multiple APIs with different auth.
160
444
  *
161
445
  * @example
446
+ * // Bearer auth for main API
447
+ * const mainClient = createClient({
448
+ * baseUrl: 'https://api.example.com',
449
+ * getToken: () => session.accessToken,
450
+ * });
451
+ *
452
+ * // API key auth for analytics
162
453
  * const analyticsClient = createClient({
163
454
  * baseUrl: 'https://analytics.example.com',
164
- * toast: { success: toast.success, error: toast.error },
165
- * navigation: useRouter,
455
+ * authMode: 'header',
456
+ * getToken: () => env.ANALYTICS_KEY,
457
+ * headerName: 'x-api-key',
166
458
  * });
167
- *
168
- * const eventsApi = createCrudApi('events', { client: analyticsClient });
169
459
  */
170
460
  declare function createClient(config: ArcClientConfig): ArcClient;
461
+ /**
462
+ * Create an `ArcClient` wired to the global `configureClient` + `configureAuth` setup.
463
+ *
464
+ * Removes the boilerplate every consumer SDK writes by hand (auth-injection adapter
465
+ * + `BaseApi` constructor with `client: { request: handleApiRequest, config: ... }`).
466
+ * The returned client reads `getToken` / `getOrgId` lazily on each request, so token
467
+ * rotation in the global auth layer takes effect immediately.
468
+ *
469
+ * Equivalent to `createClient(...)` with `getToken`/`getOrgId` pulled from
470
+ * `getAuthContext()`. Pass `overrides` to customize specific fields without
471
+ * rebuilding the whole transport.
472
+ *
473
+ * @example
474
+ * // App init (somewhere in a "use client" provider)
475
+ * configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! });
476
+ * configureAuth({
477
+ * getToken: () => session?.accessToken ?? null,
478
+ * getOrgId: () => currentOrg?.id ?? null,
479
+ * });
480
+ *
481
+ * // SDK module
482
+ * import { createAuthAwareClient } from '@classytic/arc-next/client';
483
+ * import { createCrudApi } from '@classytic/arc-next/api';
484
+ *
485
+ * const client = createAuthAwareClient();
486
+ * export const productsApi = createCrudApi('products', { client });
487
+ *
488
+ * // With per-call overrides
489
+ * const analyticsClient = createAuthAwareClient({
490
+ * baseUrl: 'https://analytics.example.com',
491
+ * authMode: 'header',
492
+ * headerName: 'x-api-key',
493
+ * });
494
+ */
495
+ declare function createAuthAwareClient(overrides?: Partial<ArcClientConfig>): ArcClient;
496
+ /**
497
+ * Get auth context for a specific client instance, falling back to global.
498
+ */
499
+ declare function getClientAuthContext(client?: ArcClient): {
500
+ token: string | null;
501
+ organizationId: string | null;
502
+ };
171
503
  /**
172
504
  * Universal API request handler.
173
505
  * Handles JSON, binary (PDF, images), CSV, and text responses.
@@ -195,4 +527,4 @@ declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: str
195
527
  */
196
528
  declare function createQueryString<T extends Record<string, unknown>>(params?: T): string;
197
529
  //#endregion
198
- export { ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, AuthConfig, ClientConfig, HttpMethod, ToastHandler, UseRouterHook, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError, isAutoIdempotency };
530
+ export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcDetailsErrorCode, ArcTopLevelErrorCode, AuthConfig, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, HttpMethod, KNOWN_DETAILS_CODES, KNOWN_TOP_LEVEL_CODES, RetryConfig, StreamUrlProtocol, TextResponse, ToastHandler, UseRouterHook, _resetAuthWarnings, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };