@classytic/arc-next 0.5.0 → 0.7.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.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { ErrorDetail } from "@classytic/repo-core/errors";
2
+
1
3
  //#region src/client.d.ts
2
4
  interface ToastHandler {
3
5
  success: (message: string) => void;
@@ -12,39 +14,33 @@ type UseRouterHook = () => {
12
14
  }) => void;
13
15
  };
14
16
  /**
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.
17
+ * Canonical error codes arc and repo-core emit on `json.code`. Single
18
+ * top-level slot — arc 2.13's `createError` lifts business codes from
19
+ * `details` to top-level so `repo-core`'s `toErrorContract` round-trips
20
+ * them on the wire. There is no separate `detailsCode` slot; everything
21
+ * lives at `error.code`.
18
22
  *
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".
23
+ * Three families compose this list:
24
+ * 1. **repo-core canonical** (`validation_error`, `not_found`, ...) RFC 7807
25
+ * / Stripe-shaped lowercase + snake_case. Cross-package universals.
26
+ * 2. **arc hierarchical** (`arc.forbidden`, `arc.validation_error`,
27
+ * `arc.org.access_denied`, ...) what arc's `errorHandlerPlugin`
28
+ * emits for HTTP-status throws + arc-classified errors.
29
+ * 3. **arc business** (`ORG_CONTEXT_REQUIRED`, `ALL_FIELDS_STRIPPED`,
30
+ * `OWNERSHIP_DENIED`, ...) emitted by mixins / org guards via
31
+ * `createError(status, msg, { code })`. The UPPER_SNAKE form is
32
+ * intentional: these are reason codes, not HTTP-status codes.
33
+ *
34
+ * `(string & {})` keeps the type open so domain packages and custom
35
+ * `errorMappers` codes still satisfy it.
41
36
  */
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"];
37
+ declare const KNOWN_ARC_ERROR_CODES: readonly ["validation_error", "not_found", "conflict", "unauthorized", "forbidden", "rate_limited", "idempotency_conflict", "precondition_failed", "internal_error", "service_unavailable", "timeout", "arc.bad_request", "arc.unauthorized", "arc.forbidden", "arc.not_found", "arc.conflict", "arc.unprocessable_entity", "arc.rate_limited", "arc.internal_error", "arc.bad_gateway", "arc.service_unavailable", "arc.gateway_timeout", "arc.validation_error", "arc.invalid_id", "arc.org.selection_required", "arc.org.access_denied", "ORG_CONTEXT_REQUIRED", "ORG_ROLE_REQUIRED", "OWNERSHIP_DENIED", "MIXED_UPDATE_SHAPE", "ALL_FIELDS_STRIPPED", "BEFORE_RESTORE_HOOK_ERROR", "duplicate_key"];
43
38
  /**
44
- * Nested business-logic error code union derived from {@link KNOWN_DETAILS_CODES}.
45
- * Same `(string & {})` escape hatch as {@link ArcTopLevelErrorCode}.
39
+ * Canonical arc error code union. `(string & {})` keeps the type open so
40
+ * domain packages can extend hierarchically (`'order.cart.locked'`,
41
+ * `'payment.gateway.timeout'`) and still satisfy the type.
46
42
  */
47
- type ArcDetailsErrorCode = (typeof KNOWN_DETAILS_CODES)[number] | (string & {});
43
+ type ArcErrorCode = (typeof KNOWN_ARC_ERROR_CODES)[number] | (string & {});
48
44
  interface ArcApiErrorOptions {
49
45
  status: number;
50
46
  statusText: string;
@@ -74,39 +70,36 @@ declare class ArcApiError extends Error {
74
70
  readonly method: HttpMethod;
75
71
  constructor(message: string, options: ArcApiErrorOptions);
76
72
  /**
77
- * Top-level error code from arc's response envelope (`json.code`).
73
+ * Canonical error code from arc's wire envelope (`json.code`).
78
74
  *
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.
75
+ * Arc 2.13 + `repo-core` 0.4 emit one canonical {@link ErrorContract}
76
+ * shape — `{ code, message, status, details? }` with the business
77
+ * code at top-level. Hosts switch on `error.code` directly:
82
78
  *
83
79
  * @example
84
- * if (error.code === 'DUPLICATE_KEY') showRetryAsAdmin();
80
+ * if (error.code === 'ORG_CONTEXT_REQUIRED') promptOrgSelector();
81
+ * if (error.code === 'arc.not_found') router.replace('/404');
82
+ * if (error.code === 'duplicate_key') showRetryAsAdmin();
85
83
  */
86
- get code(): ArcTopLevelErrorCode | null;
84
+ get code(): ArcErrorCode | null;
87
85
  /**
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
- * }
86
+ * Canonical structured details populated for validation failures
87
+ * (one entry per offending field) and duplicate-key conflicts (one entry
88
+ * per offending field). Shape matches `repo-core`'s {@link ErrorDetail}:
89
+ * `{ path?, code, message, meta? }`. Returns `null` for non-arc backends
90
+ * or responses without details.
99
91
  */
100
- get detailsCode(): ArcDetailsErrorCode | null;
92
+ get details(): readonly ErrorDetail[] | null;
101
93
  /**
102
94
  * Extract field-level validation errors as `{ field: message }` map.
103
95
  *
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.
96
+ * Reads the canonical `ErrorContract.details: ErrorDetail[]` shape first
97
+ * (what arc 2.13 + repo-core emit), then falls back to legacy shapes for
98
+ * non-arc backends:
99
+ * 1. `details: [{ path, code, message }]` — canonical (arc / repo-core).
100
+ * 2. `errors: { email: 'invalid' }` — record form (legacy app handlers).
101
+ * 3. `details: { errors: [{ field|instancePath, message }] }` — pre-2.13 AJV.
102
+ * 4. `errors: [...]` at the top level — third-party frameworks.
110
103
  */
111
104
  get fieldErrors(): Record<string, string> | null;
112
105
  }
@@ -137,23 +130,24 @@ declare function isArcApiError(error: unknown): error is ArcApiError;
137
130
  */
138
131
  declare function isAbortError(error: unknown): boolean;
139
132
  /**
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`.
133
+ * Generic check: is this an `ArcApiError` carrying a specific `code`?
134
+ * Single-slot — arc 2.13 + repo-core 0.4 emit one canonical `code` at
135
+ * top-level. Pass either the canonical lowercase form (`'arc.not_found'`,
136
+ * `'validation_error'`) or arc's UPPER_SNAKE business form
137
+ * (`'ORG_CONTEXT_REQUIRED'`).
144
138
  *
145
139
  * @example
146
- * if (isArcErrorCode(error, 'DUPLICATE_KEY')) showRetryUI();
140
+ * if (isArcErrorCode(error, 'duplicate_key')) showRetryUI();
147
141
  * if (isArcErrorCode(error, 'ORG_CONTEXT_REQUIRED')) promptOrgSelector();
148
142
  */
149
- declare function isArcErrorCode(error: unknown, code: ArcTopLevelErrorCode | ArcDetailsErrorCode): error is ArcApiError;
143
+ declare function isArcErrorCode(error: unknown, code: ArcErrorCode): error is ArcApiError;
150
144
  /**
151
145
  * Specific predicate for arc's bulk-preset + orgGuard safety code.
152
146
  *
153
147
  * Arc's bulk endpoints (`POST/PATCH/DELETE /:resource/bulk`) reject any call
154
148
  * 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.
149
+ * `403 { code: 'ORG_CONTEXT_REQUIRED', message, status: 403 }`. Hosts hitting
150
+ * this need to call `configureAuth({ getOrgId })` before retrying.
157
151
  *
158
152
  * @example
159
153
  * try { await api.bulkCreate({ data: [...] }); }
@@ -165,15 +159,16 @@ declare function isArcErrorCode(error: unknown, code: ArcTopLevelErrorCode | Arc
165
159
  */
166
160
  declare function isOrgContextRequiredError(error: unknown): error is ArcApiError;
167
161
  /**
168
- * Specific predicate for arc's `VALIDATION_ERROR` (Fastify AJV + Mongoose
162
+ * Specific predicate for validation failures (Fastify AJV + Mongoose
169
163
  * ValidationError). When true, `error.fieldErrors` is populated with the
170
- * `{ field: message }` map.
164
+ * `{ field: message }` map. Matches arc's `arc.validation_error` and the
165
+ * canonical `validation_error` from repo-core.
171
166
  */
172
167
  declare function isValidationError(error: unknown): error is ArcApiError;
173
168
  /**
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.
169
+ * Specific predicate for unique-constraint violations. Arc's errorHandler
170
+ * classifies these uniformly across MongoDB E11000, Postgres 23505,
171
+ * Prisma P2002 `arc.conflict` (with `details[].code === 'duplicate_key'`).
177
172
  */
178
173
  declare function isDuplicateKeyError(error: unknown): error is ArcApiError;
179
174
  interface ClientConfig {
@@ -327,6 +322,17 @@ declare function getAuthMode(): 'bearer' | 'cookie' | 'header';
327
322
  declare function getBaseUrl(): string;
328
323
  /** Whether auto-idempotency is enabled on the global client. */
329
324
  declare function isAutoIdempotency(): boolean;
325
+ /**
326
+ * Whether the globally-configured client carries enough auth to satisfy a
327
+ * protected endpoint without a per-request token. True when any of
328
+ * `internalApiKey`, `defaultHeaders`, or `authMode: 'cookie'` is configured.
329
+ *
330
+ * Read by `createCrudHooks` to decide whether queries should be enabled when
331
+ * `getToken()` returns null — without this, an app that authenticates via a
332
+ * global `internalApiKey` or static headers would see every query stuck in
333
+ * a permanently-disabled state, looking like a clean empty success.
334
+ */
335
+ declare function hasGlobalStaticAuth(): boolean;
330
336
  interface AuthConfig {
331
337
  /**
332
338
  * Returns the current bearer/API token, or `null` if not authenticated.
@@ -334,15 +340,83 @@ interface AuthConfig {
334
340
  * **MUST resolve synchronously.** The signature is `() => string | null`, never
335
341
  * `Promise<string | null>`. If your auth library exposes an async session getter
336
342
  * (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.
343
+ * (timer, event listener, lazy 401 retry via {@link onAuthError}) and have
344
+ * `getToken` return the *cached* value. Returning a Promise will be detected and
345
+ * logged as an Error in dev — and the underlying token will be silently treated
346
+ * as `null`, causing 401s.
340
347
  */
341
348
  getToken?: () => string | null;
342
349
  getOrgId?: () => string | null;
343
350
  /** Custom auth header name. Used when authMode is 'header'. Default: 'x-api-key' */
344
351
  headerName?: string;
352
+ /**
353
+ * Lazy 401-recovery hook. Invoked once per request when the backend returns
354
+ * 401 (or 403, if {@link retryOn403} is true) AND a handler is configured.
355
+ *
356
+ * - Return `'retry'` to re-issue the request once with a fresh token.
357
+ * - Return `'skip'` to surface the original error to the caller (e.g. after
358
+ * a refresh attempt that found no valid session — let the consumer route
359
+ * to sign-in).
360
+ * - Throw to short-circuit; the thrown error propagates to the caller.
361
+ *
362
+ * **Concurrent dedup.** When N requests hit 401 at the same time, the handler
363
+ * fires ONCE — every concurrent caller awaits the same refresh promise and
364
+ * retries with the token it produced. Avoids stampeding the refresh endpoint.
365
+ *
366
+ * **Token propagation.** Inside the handler, call `ctx.setToken(newToken)` to
367
+ * inject the refreshed token for the retry. If you've already updated your
368
+ * global auth state out-of-band (e.g. updated a signal or the auth library's
369
+ * cache), you can omit `setToken` — arc-next re-reads via `getToken()` on
370
+ * the retry attempt.
371
+ *
372
+ * @see {@link createAuthRefreshHandler} for the canonical wiring with any
373
+ * `refresh()` function (Better Auth, NextAuth, custom OAuth flows).
374
+ */
375
+ onAuthError?: AuthErrorHandler;
376
+ /**
377
+ * Treat 403 as auth-recoverable too. Default: `false` (only 401 triggers
378
+ * the handler). Enable for backends that emit 403 on expired sessions
379
+ * instead of 401.
380
+ */
381
+ retryOn403?: boolean;
382
+ /**
383
+ * Cap on auth-recovery retries per individual request. Default: `1`.
384
+ * Bumping above 1 risks pathological loops if the refresh itself
385
+ * triggers 401s — only do it if you have hard guarantees on `getToken()`.
386
+ */
387
+ maxAuthRetries?: number;
388
+ }
389
+ /**
390
+ * Context passed to {@link AuthConfig.onAuthError} when a request 401s.
391
+ *
392
+ * Inspect `error` to decide whether to refresh. Call `setToken(t)` to inject
393
+ * the refreshed token for the retry — arc-next prefers this value over
394
+ * re-reading `getToken()`, so it works even when your auth library hasn't
395
+ * yet propagated the new session.
396
+ */
397
+ interface AuthErrorContext {
398
+ /** The 401/403 thrown by the failing request. Carries `status`, `code`, `json`. */
399
+ error: ArcApiError;
400
+ /** The request that failed — useful for path-based heuristics. */
401
+ request: {
402
+ method: HttpMethod;
403
+ endpoint: string;
404
+ };
405
+ /**
406
+ * 1-indexed auth-recovery attempt. First 401 in a request lifecycle is 1.
407
+ * Reaches {@link AuthConfig.maxAuthRetries} → handler is NOT called again
408
+ * and the 401 surfaces.
409
+ */
410
+ attempt: number;
411
+ /**
412
+ * Inject the refreshed token for the retry. Pass `null` to retry
413
+ * unauthenticated. Optional — omit if `getToken()` already returns the
414
+ * fresh value.
415
+ */
416
+ setToken: (token: string | null) => void;
345
417
  }
418
+ /** {@link AuthConfig.onAuthError} signature. */
419
+ type AuthErrorHandler = (ctx: AuthErrorContext) => Promise<'retry' | 'skip'>;
346
420
  /**
347
421
  * Configure auth context for automatic token/orgId injection.
348
422
  * When configured, hooks auto-inject these values so you don't need to pass them manually.
@@ -373,6 +447,95 @@ declare function getAuthContext(): {
373
447
  };
374
448
  /** @internal — exposed for tests; resets the dev-warn dedup flag. */
375
449
  declare function _resetAuthWarnings(): void;
450
+ /**
451
+ * Outcome of a single auth-recovery cycle. `overrideToken` is the value the
452
+ * handler injected via `setToken`; `undefined` means the handler didn't call
453
+ * `setToken` and we should re-read via `getToken()`. `null` is meaningful —
454
+ * "retry without a token" (rare but valid for public endpoints).
455
+ */
456
+ interface AuthRecoveryResult {
457
+ decision: 'retry' | 'skip';
458
+ overrideToken: string | null | undefined;
459
+ }
460
+ /** @internal — exposed for tests; clears the dedup so they don't bleed. */
461
+ declare function _resetAuthRecovery(): void;
462
+ /**
463
+ * @internal
464
+ * Cross-transport access to the configured auth-recovery handler.
465
+ * Upload (XHR) and WebSocket / SSE plumbing share the same dedup as the
466
+ * fetch path — they read the handler here and call {@link _runAuthRecovery}
467
+ * when they detect a transport-specific auth failure (XHR 401, WS close
468
+ * code 1008/4401, SSE pre-flight probe 401).
469
+ */
470
+ declare function _getAuthErrorHandler(): {
471
+ handler: AuthErrorHandler | undefined;
472
+ retryOn403: boolean;
473
+ maxAuthRetries: number;
474
+ };
475
+ /**
476
+ * @internal
477
+ * Drive the shared recovery cycle from a non-fetch transport. Same dedup
478
+ * as the fetch path — concurrent callers (XHR upload + WebSocket reconnect
479
+ * + SSE probe firing at once) collapse to one refresh.
480
+ */
481
+ declare function _runAuthRecovery(handler: AuthErrorHandler, ctx: Omit<AuthErrorContext, 'setToken'>): Promise<AuthRecoveryResult>;
482
+ /**
483
+ * @internal
484
+ * Resolve the next-attempt token. Mirrors the priority in `executeRequest`'s
485
+ * auth loop — `setToken` override beats re-reading `getToken()`. Exported so
486
+ * transports outside the fetch path apply the same precedence.
487
+ */
488
+ declare function _resolveRefreshedToken(overrideToken: string | null | undefined): string | null;
489
+ /**
490
+ * @internal
491
+ * True for any error a transport should run through `onAuthError`. Matches
492
+ * the fetch path's predicate so XHR / WS / SSE failures classify the same
493
+ * way (401, or 403 when `retryOn403`).
494
+ */
495
+ declare function _isAuthRecoverable(error: unknown, retryOn403: boolean): boolean;
496
+ /**
497
+ * Build an {@link AuthErrorHandler} from any `refresh()` function that
498
+ * returns the new token (or `null` if the session is truly expired).
499
+ *
500
+ * Catches refresh errors and surfaces them as `'skip'` by default so the
501
+ * original 401 reaches the consumer instead of a misleading "refresh failed"
502
+ * trace — consumers expect to handle "session expired" once, not twice. Pass
503
+ * `onRefreshError: 'throw'` to opt in to propagation.
504
+ *
505
+ * @example Better Auth (or any session-based lib)
506
+ * ```ts
507
+ * import { configureAuth, createAuthRefreshHandler } from '@classytic/arc-next/client';
508
+ * import { authClient } from '@/lib/auth-client';
509
+ *
510
+ * configureAuth({
511
+ * getToken: () => authClient.getSession().data?.session.token ?? null,
512
+ * onAuthError: createAuthRefreshHandler({
513
+ * refresh: async () => {
514
+ * const { data } = await authClient.getSession({ disableCookieCache: true });
515
+ * return data?.session.token ?? null;
516
+ * },
517
+ * }),
518
+ * });
519
+ * ```
520
+ *
521
+ * @example Custom OAuth refresh
522
+ * ```ts
523
+ * configureAuth({
524
+ * getToken: () => tokenStore.getAccessToken(),
525
+ * onAuthError: createAuthRefreshHandler({
526
+ * refresh: () => oauthClient.refresh(tokenStore.getRefreshToken()),
527
+ * }),
528
+ * });
529
+ * ```
530
+ */
531
+ declare function createAuthRefreshHandler(opts: {
532
+ refresh: () => Promise<string | null>;
533
+ /**
534
+ * Behavior when the `refresh()` call itself throws. Default: `'skip'`
535
+ * (the original 401 surfaces; consumers handle "session expired" once).
536
+ */
537
+ onRefreshError?: 'skip' | 'throw';
538
+ }): AuthErrorHandler;
376
539
  /** Protocol family the URL should target. `http` keeps `getBaseUrl()` as-is; `ws` rewrites `http(s)://` → `ws(s)://`. */
377
540
  type StreamUrlProtocol = 'http' | 'ws';
378
541
  /**
@@ -505,8 +668,8 @@ declare function getClientAuthContext(client?: ArcClient): {
505
668
  * Handles JSON, binary (PDF, images), CSV, and text responses.
506
669
  *
507
670
  * @example
508
- * const { success, data } = await handleApiRequest<ApiResponse<User>>('GET', '/users/me');
509
- * const response = await handleApiRequest<PaginatedResponse<Product>>('GET', '/products?page=1');
671
+ * const user = await handleApiRequest<User>('GET', '/users/me');
672
+ * const response = await handleApiRequest<PaginatedResult<Product>>('GET', '/products?page=1');
510
673
  */
511
674
  declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: string, options?: ApiRequestOptions): Promise<T>;
512
675
  /**
@@ -526,5 +689,110 @@ declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: str
526
689
  * // => 'populate[employeeId][select]=name,email'
527
690
  */
528
691
  declare function createQueryString<T extends Record<string, unknown>>(params?: T): string;
692
+ /**
693
+ * Returns the auth headers arc-next would inject on a fetch right now —
694
+ * `Authorization` (or the configured custom `headerName` for `authMode:
695
+ * 'header'`), `x-organization-id`, and `x-internal-api-key`. Use for the
696
+ * rare case where you need full `Response` control via plain `fetch` but
697
+ * still want arc-next's auth wiring.
698
+ *
699
+ * @example
700
+ * const res = await fetch(url, {
701
+ * headers: { ...arcAuthHeaders(), 'X-Custom': '1' },
702
+ * credentials: getAuthMode() === 'cookie' ? 'include' : 'same-origin',
703
+ * });
704
+ */
705
+ declare function arcAuthHeaders(): Record<string, string>;
706
+ interface ArcFetchOptions extends Omit<RequestInit, 'body' | 'headers'> {
707
+ /**
708
+ * Request body. Plain objects and arrays are auto-`JSON.stringify`d and
709
+ * sent with `Content-Type: application/json`. Binary bodies (`FormData`,
710
+ * `Blob`, `ArrayBuffer`, `URLSearchParams`, `ReadableStream`) and strings
711
+ * pass through unchanged — the caller owns `Content-Type` for those.
712
+ */
713
+ body?: Record<string, unknown> | unknown[] | BodyInit | null;
714
+ /**
715
+ * Extra headers merged on top of arc-next's auto-injected ones. Auth-
716
+ * related headers (`Authorization`, `x-organization-id`,
717
+ * `x-internal-api-key`, and the configured custom-auth header name) are
718
+ * **protected** — passing them here is silently dropped, so a caller can't
719
+ * accidentally strip the bearer token by spreading their own header map.
720
+ */
721
+ headers?: Record<string, string>;
722
+ /** AbortSignal — same semantics as `fetch`. */
723
+ signal?: AbortSignal;
724
+ /** Per-call elevated-scope override (`x-arc-scope: platform`). */
725
+ elevated?: boolean;
726
+ /** Per-call `Idempotency-Key` header. */
727
+ idempotencyKey?: string;
728
+ /** Next.js fetch `revalidate` pass-through. */
729
+ revalidate?: number;
730
+ /** Next.js fetch `tags` pass-through. */
731
+ tags?: string[];
732
+ /** `cache` pass-through (browser fetch + Next.js). */
733
+ cache?: RequestCache;
734
+ /**
735
+ * Per-call client. Defaults to a lazily-created `createAuthAwareClient()`
736
+ * that bridges global `configureClient` + `configureAuth`. Pass a
737
+ * dedicated `createClient(...)` for multi-backend apps.
738
+ */
739
+ client?: ArcClient;
740
+ }
741
+ /** @internal — tests reset the default client between cases. */
742
+ declare function _resetArcFetchClient(): void;
743
+ /**
744
+ * Authenticated, tenant-scoped fetch to an arc endpoint — one line for the
745
+ * non-hook contexts where `useQuery` / `useMutation` aren't available
746
+ * (event handlers, service workers, server actions, custom MDX submits,
747
+ * background polls).
748
+ *
749
+ * Auto-injects on every call:
750
+ * - `Authorization: Bearer <token>` (from `configureAuth().getToken`), or
751
+ * the custom header for `authMode: 'header'`
752
+ * - `x-organization-id` (from `configureAuth().getOrgId`)
753
+ * - `Content-Type: application/json` (only for plain object/array bodies)
754
+ * - `x-internal-api-key`, `Accept-Version`, `Idempotency-Key`,
755
+ * `x-arc-scope` when configured
756
+ *
757
+ * Composes with everything else `configureClient` + `configureAuth` do:
758
+ * - `retry` (5xx backoff)
759
+ * - `onAuthError` (401 → refresh → retry, with concurrent dedup)
760
+ * - `beforeRequest` / `afterResponse` interceptors
761
+ * - `cookie` / `bearer` / `header` auth modes
762
+ *
763
+ * Response handling:
764
+ * - 2xx → parsed body (JSON for `application/json`, Blob for binary,
765
+ * text for `text/*`).
766
+ * - non-2xx → throws `ArcApiError` with parsed body, status, endpoint,
767
+ * method. Use `isArcApiError(err)` + `err.code` to discriminate.
768
+ *
769
+ * For full `Response` control (rare), use plain `fetch` with
770
+ * {@link arcAuthHeaders} instead.
771
+ *
772
+ * @example
773
+ * import { arc } from '@classytic/arc-next/client';
774
+ *
775
+ * // Before — 15 lines of header dance + error parse + JSON
776
+ * // After:
777
+ * const result = await arc.post<{ ok: true }>('/api/statements', statements);
778
+ */
779
+ declare function arcFetch<T = unknown>(path: string, options?: ArcFetchOptions): Promise<T>;
780
+ /**
781
+ * Method-specific shorthands for the 90% case. Each mirrors `arcFetch` with
782
+ * the HTTP verb pre-filled; mutating verbs accept `body` as the second arg
783
+ * so the call reads as a sentence:
784
+ *
785
+ * `arc.post('/path', payload)` instead of
786
+ * `arcFetch('/path', { method: 'POST', body: payload })`.
787
+ *
788
+ * Identical composition with `onAuthError`, retry, and interceptors.
789
+ */
790
+ declare const arc: {
791
+ get: <T = unknown>(path: string, opts?: ArcFetchOptions) => Promise<T>;
792
+ post: <T = unknown>(path: string, body?: ArcFetchOptions["body"], opts?: ArcFetchOptions) => Promise<T>;
793
+ put: <T = unknown>(path: string, body?: ArcFetchOptions["body"], opts?: ArcFetchOptions) => Promise<T>;
794
+ patch: <T = unknown>(path: string, body?: ArcFetchOptions["body"], opts?: ArcFetchOptions) => Promise<T>;
795
+ delete: <T = unknown>(path: string, opts?: ArcFetchOptions) => Promise<T>;
796
+ };
529
797
  //#endregion
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 };
798
+ export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcErrorCode, ArcFetchOptions, AuthConfig, AuthErrorContext, AuthErrorHandler, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, HttpMethod, KNOWN_ARC_ERROR_CODES, RetryConfig, StreamUrlProtocol, TextResponse, ToastHandler, UseRouterHook, _getAuthErrorHandler, _isAuthRecoverable, _resetArcFetchClient, _resetAuthRecovery, _resetAuthWarnings, _resolveRefreshedToken, _runAuthRecovery, arc, arcAuthHeaders, arcFetch, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createAuthRefreshHandler, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, hasGlobalStaticAuth, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };