@classytic/arc-next 0.4.1 → 0.6.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/client.js CHANGED
@@ -1,5 +1,61 @@
1
1
  //#region src/client.ts
2
2
  /**
3
+ * Canonical error codes arc and repo-core emit on `json.code`. Single
4
+ * top-level slot — arc 2.13's `createError` lifts business codes from
5
+ * `details` to top-level so `repo-core`'s `toErrorContract` round-trips
6
+ * them on the wire. There is no separate `detailsCode` slot; everything
7
+ * lives at `error.code`.
8
+ *
9
+ * Three families compose this list:
10
+ * 1. **repo-core canonical** (`validation_error`, `not_found`, ...) — RFC 7807
11
+ * / Stripe-shaped lowercase + snake_case. Cross-package universals.
12
+ * 2. **arc hierarchical** (`arc.forbidden`, `arc.validation_error`,
13
+ * `arc.org.access_denied`, ...) — what arc's `errorHandlerPlugin`
14
+ * emits for HTTP-status throws + arc-classified errors.
15
+ * 3. **arc business** (`ORG_CONTEXT_REQUIRED`, `ALL_FIELDS_STRIPPED`,
16
+ * `OWNERSHIP_DENIED`, ...) — emitted by mixins / org guards via
17
+ * `createError(status, msg, { code })`. The UPPER_SNAKE form is
18
+ * intentional: these are reason codes, not HTTP-status codes.
19
+ *
20
+ * `(string & {})` keeps the type open so domain packages and custom
21
+ * `errorMappers` codes still satisfy it.
22
+ */
23
+ const KNOWN_ARC_ERROR_CODES = [
24
+ "validation_error",
25
+ "not_found",
26
+ "conflict",
27
+ "unauthorized",
28
+ "forbidden",
29
+ "rate_limited",
30
+ "idempotency_conflict",
31
+ "precondition_failed",
32
+ "internal_error",
33
+ "service_unavailable",
34
+ "timeout",
35
+ "arc.bad_request",
36
+ "arc.unauthorized",
37
+ "arc.forbidden",
38
+ "arc.not_found",
39
+ "arc.conflict",
40
+ "arc.unprocessable_entity",
41
+ "arc.rate_limited",
42
+ "arc.internal_error",
43
+ "arc.bad_gateway",
44
+ "arc.service_unavailable",
45
+ "arc.gateway_timeout",
46
+ "arc.validation_error",
47
+ "arc.invalid_id",
48
+ "arc.org.selection_required",
49
+ "arc.org.access_denied",
50
+ "ORG_CONTEXT_REQUIRED",
51
+ "ORG_ROLE_REQUIRED",
52
+ "OWNERSHIP_DENIED",
53
+ "MIXED_UPDATE_SHAPE",
54
+ "ALL_FIELDS_STRIPPED",
55
+ "BEFORE_RESTORE_HOOK_ERROR",
56
+ "duplicate_key"
57
+ ];
58
+ /**
3
59
  * Rich API error with status code, response payload, and request metadata.
4
60
  * Extends Error so existing `catch (e) { if (e instanceof Error) }` still works.
5
61
  *
@@ -28,9 +84,71 @@ var ArcApiError = class extends Error {
28
84
  this.endpoint = options.endpoint;
29
85
  this.method = options.method;
30
86
  }
31
- /** Extract field-level validation errors if present (e.g. `{ field: "message" }`) */
87
+ /**
88
+ * Canonical error code from arc's wire envelope (`json.code`).
89
+ *
90
+ * Arc 2.13 + `repo-core` 0.4 emit one canonical {@link ErrorContract}
91
+ * shape — `{ code, message, status, details? }` — with the business
92
+ * code at top-level. Hosts switch on `error.code` directly:
93
+ *
94
+ * @example
95
+ * if (error.code === 'ORG_CONTEXT_REQUIRED') promptOrgSelector();
96
+ * if (error.code === 'arc.not_found') router.replace('/404');
97
+ * if (error.code === 'duplicate_key') showRetryAsAdmin();
98
+ */
99
+ get code() {
100
+ const j = this.json;
101
+ return j && typeof j.code === "string" ? j.code : null;
102
+ }
103
+ /**
104
+ * Canonical structured details — populated for validation failures
105
+ * (one entry per offending field) and duplicate-key conflicts (one entry
106
+ * per offending field). Shape matches `repo-core`'s {@link ErrorDetail}:
107
+ * `{ path?, code, message, meta? }`. Returns `null` for non-arc backends
108
+ * or responses without details.
109
+ */
110
+ get details() {
111
+ const j = this.json;
112
+ return Array.isArray(j?.details) ? j.details : null;
113
+ }
114
+ /**
115
+ * Extract field-level validation errors as `{ field: message }` map.
116
+ *
117
+ * Reads the canonical `ErrorContract.details: ErrorDetail[]` shape first
118
+ * (what arc 2.13 + repo-core emit), then falls back to legacy shapes for
119
+ * non-arc backends:
120
+ * 1. `details: [{ path, code, message }]` — canonical (arc / repo-core).
121
+ * 2. `errors: { email: 'invalid' }` — record form (legacy app handlers).
122
+ * 3. `details: { errors: [{ field|instancePath, message }] }` — pre-2.13 AJV.
123
+ * 4. `errors: [...]` at the top level — third-party frameworks.
124
+ */
32
125
  get fieldErrors() {
33
- return this.json?.errors ?? null;
126
+ const j = this.json;
127
+ if (!j) return null;
128
+ if (Array.isArray(j.details) && j.details.length > 0) {
129
+ const map = {};
130
+ for (const d of j.details) {
131
+ if (!d || typeof d !== "object") continue;
132
+ const key = typeof d.path === "string" ? d.path : "";
133
+ const msg = typeof d.message === "string" ? d.message : "Invalid value";
134
+ if (key && !(key in map)) map[key] = msg;
135
+ }
136
+ if (Object.keys(map).length > 0) return map;
137
+ }
138
+ if (j.errors && !Array.isArray(j.errors) && typeof j.errors === "object") return j.errors;
139
+ const detailsObj = j.details;
140
+ const errorList = Array.isArray(j.errors) ? j.errors : Array.isArray(detailsObj?.errors) ? detailsObj.errors : null;
141
+ if (!errorList) return null;
142
+ const map = {};
143
+ for (const item of errorList) {
144
+ if (!item || typeof item !== "object") continue;
145
+ const e = item;
146
+ const key = typeof e.field === "string" && e.field || typeof e.instancePath === "string" && e.instancePath.replace(/^\//, "") || typeof e.path === "string" && e.path || typeof e.params?.missingProperty === "string" && e.params.missingProperty || "";
147
+ if (!key) continue;
148
+ const msg = typeof e.message === "string" ? e.message : "Invalid value";
149
+ if (!(key in map)) map[key] = msg;
150
+ }
151
+ return Object.keys(map).length > 0 ? map : null;
34
152
  }
35
153
  };
36
154
  /**
@@ -39,6 +157,91 @@ var ArcApiError = class extends Error {
39
157
  function isArcApiError(error) {
40
158
  return error instanceof ArcApiError;
41
159
  }
160
+ /**
161
+ * Detects request cancellation (AbortSignal triggered) regardless of runtime.
162
+ *
163
+ * Filtering out abort errors from real failures is a common need — without it,
164
+ * unmounting a React component mid-fetch produces noisy logs and bogus error
165
+ * toasts that look like API failures. Use this predicate to skip the catch
166
+ * branch when the request was deliberately cancelled.
167
+ *
168
+ * Handles the three AbortError shapes you'll see in the wild:
169
+ * 1. Browser `DOMException { name: 'AbortError' }`
170
+ * 2. Node 18+ / undici `Error { name: 'AbortError' }` (no DOMException)
171
+ * 3. Some polyfills / older runtimes where the error has `code: 'ERR_ABORTED'`
172
+ *
173
+ * @example
174
+ * try {
175
+ * await api.getAll({ options: { signal } });
176
+ * } catch (err) {
177
+ * if (isAbortError(err)) return; // user navigated away — silence
178
+ * showToast('Failed to load: ' + err.message);
179
+ * }
180
+ */
181
+ function isAbortError(error) {
182
+ if (!error) return false;
183
+ if (typeof error !== "object") return false;
184
+ const e = error;
185
+ if (e.name === "AbortError") return true;
186
+ if (e.code === "ERR_ABORTED") return true;
187
+ return false;
188
+ }
189
+ /**
190
+ * Generic check: is this an `ArcApiError` carrying a specific `code`?
191
+ * Single-slot — arc 2.13 + repo-core 0.4 emit one canonical `code` at
192
+ * top-level. Pass either the canonical lowercase form (`'arc.not_found'`,
193
+ * `'validation_error'`) or arc's UPPER_SNAKE business form
194
+ * (`'ORG_CONTEXT_REQUIRED'`).
195
+ *
196
+ * @example
197
+ * if (isArcErrorCode(error, 'duplicate_key')) showRetryUI();
198
+ * if (isArcErrorCode(error, 'ORG_CONTEXT_REQUIRED')) promptOrgSelector();
199
+ */
200
+ function isArcErrorCode(error, code) {
201
+ return isArcApiError(error) && error.code === code;
202
+ }
203
+ /**
204
+ * Specific predicate for arc's bulk-preset + orgGuard safety code.
205
+ *
206
+ * Arc's bulk endpoints (`POST/PATCH/DELETE /:resource/bulk`) reject any call
207
+ * where `request.scope.organizationId` is missing — the wire signal is
208
+ * `403 { code: 'ORG_CONTEXT_REQUIRED', message, status: 403 }`. Hosts hitting
209
+ * this need to call `configureAuth({ getOrgId })` before retrying.
210
+ *
211
+ * @example
212
+ * try { await api.bulkCreate({ data: [...] }); }
213
+ * catch (e) {
214
+ * if (isOrgContextRequiredError(e)) {
215
+ * console.warn('Bulk requires org context. Configure: configureAuth({ getOrgId })');
216
+ * } else throw e;
217
+ * }
218
+ */
219
+ function isOrgContextRequiredError(error) {
220
+ return isArcErrorCode(error, "ORG_CONTEXT_REQUIRED");
221
+ }
222
+ /**
223
+ * Specific predicate for validation failures (Fastify AJV + Mongoose
224
+ * ValidationError). When true, `error.fieldErrors` is populated with the
225
+ * `{ field: message }` map. Matches arc's `arc.validation_error` and the
226
+ * canonical `validation_error` from repo-core.
227
+ */
228
+ function isValidationError(error) {
229
+ if (!isArcApiError(error)) return false;
230
+ return error.code === "arc.validation_error" || error.code === "validation_error";
231
+ }
232
+ /**
233
+ * Specific predicate for unique-constraint violations. Arc's errorHandler
234
+ * classifies these uniformly across MongoDB E11000, Postgres 23505,
235
+ * Prisma P2002 → `arc.conflict` (with `details[].code === 'duplicate_key'`).
236
+ */
237
+ function isDuplicateKeyError(error) {
238
+ if (!isArcApiError(error)) return false;
239
+ if (error.code === "arc.conflict" || error.code === "duplicate_key" || error.code === "conflict") {
240
+ if (error.code === "arc.conflict") return error.details?.some((d) => d.code === "duplicate_key") ?? false;
241
+ return true;
242
+ }
243
+ return false;
244
+ }
42
245
  let clientConfig = null;
43
246
  /**
44
247
  * Configure the API client. Call once at app init before any API requests.
@@ -73,17 +276,22 @@ function isAutoIdempotency() {
73
276
  return clientConfig?.autoIdempotency ?? false;
74
277
  }
75
278
  let authConfig = null;
279
+ let hasWarnedAsyncToken = false;
76
280
  /**
77
281
  * Configure auth context for automatic token/orgId injection.
78
282
  * When configured, hooks auto-inject these values so you don't need to pass them manually.
79
283
  *
80
284
  * **SSR safety:** This sets module-level state. Call only in client-side code.
81
285
  *
286
+ * **Token resolution is synchronous.** `getToken` must return `string | null`
287
+ * synchronously — never a Promise. See {@link AuthConfig.getToken} for guidance
288
+ * on bridging async auth libraries via cached values.
289
+ *
82
290
  * @example
83
291
  * // Cookie auth (no token needed)
84
292
  * configureAuth({ getOrgId: () => currentOrg.id });
85
293
  *
86
- * // Bearer auth
294
+ * // Bearer auth — token is cached synchronously by the auth library
87
295
  * configureAuth({
88
296
  * getToken: () => session?.accessToken ?? null,
89
297
  * getOrgId: () => currentOrg?.id ?? null,
@@ -92,16 +300,58 @@ let authConfig = null;
92
300
  function configureAuth(config) {
93
301
  if (typeof window === "undefined") console.warn("[arc-next] configureAuth() called on the server. This sets module-level state that persists across requests. Call only in client-side code (e.g., a 'use client' provider).");
94
302
  authConfig = config;
303
+ hasWarnedAsyncToken = false;
304
+ }
305
+ function readToken(getToken) {
306
+ if (!getToken) return null;
307
+ const result = getToken();
308
+ if (result && typeof result.then === "function") {
309
+ if (!hasWarnedAsyncToken) {
310
+ hasWarnedAsyncToken = true;
311
+ console.warn("[arc-next] configureAuth({ getToken }) returned a Promise. Tokens MUST resolve synchronously — async returns are dropped and requests will be unauthenticated. Cache your token (localStorage, memory, signal) and have getToken() return the cached value.");
312
+ }
313
+ return null;
314
+ }
315
+ return result ?? null;
95
316
  }
96
317
  /**
97
318
  * Get the current auth context. Returns nulls when not configured.
98
319
  */
99
320
  function getAuthContext() {
100
321
  return {
101
- token: authConfig?.getToken?.() ?? null,
322
+ token: readToken(authConfig?.getToken),
102
323
  organizationId: authConfig?.getOrgId?.() ?? null
103
324
  };
104
325
  }
326
+ /** @internal — exposed for tests; resets the dev-warn dedup flag. */
327
+ function _resetAuthWarnings() {
328
+ hasWarnedAsyncToken = false;
329
+ }
330
+ /**
331
+ * Build an auth-aware URL using the global client + auth singletons.
332
+ *
333
+ * Single source of truth for {@link import('./sse.js').buildSseUrl} (HTTP) and
334
+ * {@link import('./ws.js').buildWsUrl} (WebSocket). Both delegate here so the
335
+ * auth-injection rule (org always, token only when `authMode !== 'cookie'`)
336
+ * stays in one place and can't drift.
337
+ *
338
+ * @param path Path appended to the base URL (leading slash recommended).
339
+ * @param params Caller-supplied params; merged with auth params. Caller wins on key collision.
340
+ * @param protocol `'http'` (default) or `'ws'` — controls the protocol rewrite.
341
+ */
342
+ function buildStreamUrl(path, params = {}, protocol = "http") {
343
+ const auth = getAuthContext();
344
+ const qs = new URLSearchParams();
345
+ for (const [key, value] of Object.entries(params)) {
346
+ if (value === void 0 || value === null || value === "") continue;
347
+ qs.set(key, String(value));
348
+ }
349
+ if (auth.organizationId && !qs.has("organizationId")) qs.set("organizationId", auth.organizationId);
350
+ if (auth.token && getAuthMode() !== "cookie" && !qs.has("token")) qs.set("token", auth.token);
351
+ const origin = protocol === "ws" ? getBaseUrl().replace(/^http(s?):\/\//, "ws$1://") : getBaseUrl();
352
+ const suffix = qs.toString();
353
+ return `${origin}${path}${suffix ? `?${suffix}` : ""}`;
354
+ }
105
355
  /**
106
356
  * Create an isolated API client for a specific backend.
107
357
  * Use this when your app needs to talk to multiple APIs with different auth.
@@ -132,7 +382,7 @@ function createClient(config) {
132
382
  request: (method, endpoint, options) => {
133
383
  if (clientAuth) {
134
384
  const resolved = { ...options };
135
- if (resolved.token === void 0 && clientAuth.getToken) resolved.token = clientAuth.getToken();
385
+ if (resolved.token === void 0 && clientAuth.getToken) resolved.token = readToken(clientAuth.getToken);
136
386
  if (resolved.organizationId === void 0 && clientAuth.getOrgId) resolved.organizationId = clientAuth.getOrgId();
137
387
  if (clientCfg.authMode === "header" && resolved.token) {
138
388
  resolved.headerOptions = {
@@ -152,17 +402,115 @@ function createClient(config) {
152
402
  };
153
403
  }
154
404
  /**
405
+ * Create an `ArcClient` wired to the global `configureClient` + `configureAuth` setup.
406
+ *
407
+ * Removes the boilerplate every consumer SDK writes by hand (auth-injection adapter
408
+ * + `BaseApi` constructor with `client: { request: handleApiRequest, config: ... }`).
409
+ * The returned client reads `getToken` / `getOrgId` lazily on each request, so token
410
+ * rotation in the global auth layer takes effect immediately.
411
+ *
412
+ * Equivalent to `createClient(...)` with `getToken`/`getOrgId` pulled from
413
+ * `getAuthContext()`. Pass `overrides` to customize specific fields without
414
+ * rebuilding the whole transport.
415
+ *
416
+ * @example
417
+ * // App init (somewhere in a "use client" provider)
418
+ * configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! });
419
+ * configureAuth({
420
+ * getToken: () => session?.accessToken ?? null,
421
+ * getOrgId: () => currentOrg?.id ?? null,
422
+ * });
423
+ *
424
+ * // SDK module
425
+ * import { createAuthAwareClient } from '@classytic/arc-next/client';
426
+ * import { createCrudApi } from '@classytic/arc-next/api';
427
+ *
428
+ * const client = createAuthAwareClient();
429
+ * export const productsApi = createCrudApi('products', { client });
430
+ *
431
+ * // With per-call overrides
432
+ * const analyticsClient = createAuthAwareClient({
433
+ * baseUrl: 'https://analytics.example.com',
434
+ * authMode: 'header',
435
+ * headerName: 'x-api-key',
436
+ * });
437
+ */
438
+ function createAuthAwareClient(overrides = {}) {
439
+ return createClient({
440
+ baseUrl: overrides.baseUrl ?? getBaseUrl(),
441
+ authMode: overrides.authMode ?? getAuthMode(),
442
+ autoIdempotency: overrides.autoIdempotency ?? isAutoIdempotency(),
443
+ elevated: overrides.elevated ?? clientConfig?.elevated,
444
+ ...overrides,
445
+ getToken: overrides.getToken ?? (() => readToken(authConfig?.getToken)),
446
+ getOrgId: overrides.getOrgId ?? (() => authConfig?.getOrgId?.() ?? null),
447
+ headerName: overrides.headerName ?? authConfig?.headerName
448
+ });
449
+ }
450
+ /**
155
451
  * Get auth context for a specific client instance, falling back to global.
156
452
  */
157
453
  function getClientAuthContext(client) {
158
454
  if (client?.auth) return {
159
- token: client.auth.getToken?.() ?? authConfig?.getToken?.() ?? null,
455
+ token: readToken(client.auth.getToken) ?? readToken(authConfig?.getToken),
160
456
  organizationId: client.auth.getOrgId?.() ?? authConfig?.getOrgId?.() ?? null
161
457
  };
162
458
  return getAuthContext();
163
459
  }
460
+ /** Default retry predicate: retry on network failures and 5xx, never on Abort or 4xx. */
461
+ function defaultShouldRetry(error) {
462
+ if (isAbortError(error)) return false;
463
+ if (isArcApiError(error)) return error.status >= 500 && error.status < 600;
464
+ return error instanceof Error;
465
+ }
466
+ /** Compute the delay in ms before the Nth retry. */
467
+ function computeBackoff(retry, attempt) {
468
+ const strategy = retry.backoff ?? "exponential";
469
+ if (typeof strategy === "function") return Math.max(0, strategy(attempt));
470
+ if (strategy === "linear") return 300 * (attempt + 1);
471
+ return Math.min(300 * Math.pow(2, attempt), 1e4);
472
+ }
164
473
  async function executeRequest(config, method, endpoint, options = {}) {
165
- const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal, idempotencyKey } = options;
474
+ const totalAttempts = Math.max(1, config.retry?.attempts ?? 1);
475
+ const shouldRetry = (() => {
476
+ const r = config.retry?.retryOn;
477
+ if (typeof r === "function") return r;
478
+ if (Array.isArray(r)) return (e) => isArcApiError(e) && r.includes(e.status);
479
+ return defaultShouldRetry;
480
+ })();
481
+ let lastError;
482
+ for (let attempt = 0; attempt < totalAttempts; attempt++) try {
483
+ return await executeAttempt(config, method, endpoint, options, attempt);
484
+ } catch (error) {
485
+ lastError = error;
486
+ if (attempt === totalAttempts - 1 || !shouldRetry(error)) throw error;
487
+ const delay = computeBackoff(config.retry ?? {}, attempt);
488
+ if (delay > 0) await sleepAbortable(delay, options.signal);
489
+ }
490
+ throw lastError;
491
+ }
492
+ /** Sleep that resolves early if the signal aborts. */
493
+ function sleepAbortable(ms, signal) {
494
+ return new Promise((resolve, reject) => {
495
+ if (signal?.aborted) {
496
+ reject(Object.assign(/* @__PURE__ */ new Error("Aborted"), { name: "AbortError" }));
497
+ return;
498
+ }
499
+ const timer = setTimeout(() => {
500
+ signal?.removeEventListener("abort", onAbort);
501
+ resolve();
502
+ }, ms);
503
+ const onAbort = () => {
504
+ clearTimeout(timer);
505
+ reject(Object.assign(/* @__PURE__ */ new Error("Aborted"), { name: "AbortError" }));
506
+ };
507
+ signal?.addEventListener("abort", onAbort, { once: true });
508
+ });
509
+ }
510
+ /** A single fetch attempt — used by executeRequest's retry loop. */
511
+ async function executeAttempt(config, method, endpoint, options, attempt) {
512
+ const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal, idempotencyKey, elevated } = options;
513
+ const startTime = Date.now();
166
514
  try {
167
515
  let headers = {
168
516
  ...organizationId ? { "x-organization-id": organizationId } : {},
@@ -175,19 +523,34 @@ async function executeRequest(config, method, endpoint, options = {}) {
175
523
  } else headers["Authorization"] = `Bearer ${token}`;
176
524
  if (config.apiVersion) headers["Accept-Version"] = config.apiVersion;
177
525
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
526
+ if (elevated ?? config.elevated ?? false) headers["x-arc-scope"] = "platform";
178
527
  if (body !== void 0 && body !== null && !(body instanceof FormData)) headers["Content-Type"] = "application/json";
179
528
  if (headerOptions) headers = {
180
529
  ...headers,
181
530
  ...headerOptions
182
531
  };
183
532
  const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
533
+ let serializedBody = void 0;
534
+ if (body !== void 0 && body !== null) serializedBody = body instanceof FormData ? body : JSON.stringify(body);
535
+ if (config.beforeRequest) {
536
+ const ctx = await config.beforeRequest({
537
+ method,
538
+ endpoint,
539
+ headers,
540
+ body: serializedBody,
541
+ signal,
542
+ attempt
543
+ });
544
+ headers = ctx.headers;
545
+ serializedBody = ctx.body;
546
+ }
184
547
  const fetchOptions = {
185
548
  method,
186
549
  headers,
187
550
  credentials,
188
551
  ...signal ? { signal } : {}
189
552
  };
190
- if (body !== void 0 && body !== null) fetchOptions.body = body instanceof FormData ? body : JSON.stringify(body);
553
+ if (serializedBody !== void 0) fetchOptions.body = serializedBody;
191
554
  if (cache) fetchOptions.cache = cache;
192
555
  if (revalidate !== void 0) fetchOptions.next = {
193
556
  ...fetchOptions.next,
@@ -203,7 +566,8 @@ async function executeRequest(config, method, endpoint, options = {}) {
203
566
  let errorMessage = response.statusText;
204
567
  try {
205
568
  json = await response.clone().json();
206
- errorMessage = json?.message || response.statusText;
569
+ const j = json;
570
+ errorMessage = typeof j?.error === "string" && j.error || typeof j?.message === "string" && j.message || response.statusText;
207
571
  } catch {
208
572
  try {
209
573
  const text = await response.text();
@@ -251,6 +615,14 @@ async function executeRequest(config, method, endpoint, options = {}) {
251
615
  throw new Error(`Failed to parse response body from ${method} ${endpoint}: blob error: ${blobError instanceof Error ? blobError.message : String(blobError)}`);
252
616
  }
253
617
  }
618
+ if (config.afterResponse) data = (await config.afterResponse({
619
+ method,
620
+ endpoint,
621
+ status: response.status,
622
+ body: data,
623
+ durationMs: Date.now() - startTime,
624
+ response
625
+ })).body;
254
626
  return data;
255
627
  } catch (error) {
256
628
  if (error instanceof Error) throw error;
@@ -262,8 +634,8 @@ async function executeRequest(config, method, endpoint, options = {}) {
262
634
  * Handles JSON, binary (PDF, images), CSV, and text responses.
263
635
  *
264
636
  * @example
265
- * const { success, data } = await handleApiRequest<ApiResponse<User>>('GET', '/users/me');
266
- * const response = await handleApiRequest<PaginatedResponse<Product>>('GET', '/products?page=1');
637
+ * const user = await handleApiRequest<User>('GET', '/users/me');
638
+ * const response = await handleApiRequest<PaginatedResult<Product>>('GET', '/products?page=1');
267
639
  */
268
640
  async function handleApiRequest(method, endpoint, options = {}) {
269
641
  if (!clientConfig) throw new Error("arc-next: Client not configured. Call configureClient({ baseUrl }) before making API requests.");
@@ -308,10 +680,18 @@ function createQueryString(params = {}) {
308
680
  if (value.length > 1) searchParams.append(`${key}[in]`, value.join(","));
309
681
  else if (value.length === 1) searchParams.append(key, String(value[0]));
310
682
  } else if (value === null) searchParams.append(key, "null");
683
+ else if (typeof value === "object") for (const [op, opValue] of Object.entries(value)) {
684
+ if (opValue === void 0 || opValue === "") continue;
685
+ const bracketKey = `${key}[${op}]`;
686
+ if (Array.isArray(opValue)) {
687
+ if (opValue.length > 0) searchParams.append(bracketKey, opValue.join(","));
688
+ } else if (opValue === null) searchParams.append(bracketKey, "null");
689
+ else searchParams.append(bracketKey, String(opValue));
690
+ }
311
691
  else searchParams.append(key, String(value));
312
692
  });
313
693
  return searchParams.toString();
314
694
  }
315
695
 
316
696
  //#endregion
317
- export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isArcApiError, isAutoIdempotency };
697
+ export { ArcApiError, KNOWN_ARC_ERROR_CODES, _resetAuthWarnings, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };