@classytic/arc-next 0.6.0 → 0.7.1

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
@@ -275,6 +275,23 @@ function getBaseUrl() {
275
275
  function isAutoIdempotency() {
276
276
  return clientConfig?.autoIdempotency ?? false;
277
277
  }
278
+ /**
279
+ * Whether the globally-configured client carries enough auth to satisfy a
280
+ * protected endpoint without a per-request token. True when any of
281
+ * `internalApiKey`, `defaultHeaders`, or `authMode: 'cookie'` is configured.
282
+ *
283
+ * Read by `createCrudHooks` to decide whether queries should be enabled when
284
+ * `getToken()` returns null — without this, an app that authenticates via a
285
+ * global `internalApiKey` or static headers would see every query stuck in
286
+ * a permanently-disabled state, looking like a clean empty success.
287
+ */
288
+ function hasGlobalStaticAuth() {
289
+ if (!clientConfig) return false;
290
+ if (clientConfig.authMode === "cookie") return true;
291
+ if (clientConfig.internalApiKey) return true;
292
+ if (clientConfig.defaultHeaders && Object.keys(clientConfig.defaultHeaders).length > 0) return true;
293
+ return false;
294
+ }
278
295
  let authConfig = null;
279
296
  let hasWarnedAsyncToken = false;
280
297
  /**
@@ -308,7 +325,7 @@ function readToken(getToken) {
308
325
  if (result && typeof result.then === "function") {
309
326
  if (!hasWarnedAsyncToken) {
310
327
  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.");
328
+ console.error(/* @__PURE__ */ new Error("[arc-next] configureAuth({ getToken }) returned a Promise. Tokens MUST resolve synchronously — async returns are dropped and every authenticated query will be silently disabled (no GET fires, isLoading:false, item:null). Fix: cache the token outside getToken (localStorage, memory, signal, useState) and have getToken() return the cached value. See README → 'Authentication'."));
312
329
  }
313
330
  return null;
314
331
  }
@@ -328,6 +345,138 @@ function _resetAuthWarnings() {
328
345
  hasWarnedAsyncToken = false;
329
346
  }
330
347
  /**
348
+ * Shared in-flight refresh. Multiple concurrent 401s collapse onto one
349
+ * `onAuthError` invocation — the dedup happens here. Cleared in `.finally`
350
+ * so the NEXT 401 (after settlement) triggers a fresh recovery cycle.
351
+ */
352
+ let pendingAuthRecovery = null;
353
+ /** @internal — exposed for tests; clears the dedup so they don't bleed. */
354
+ function _resetAuthRecovery() {
355
+ pendingAuthRecovery = null;
356
+ }
357
+ /**
358
+ * @internal
359
+ * Cross-transport access to the configured auth-recovery handler.
360
+ * Upload (XHR) and WebSocket / SSE plumbing share the same dedup as the
361
+ * fetch path — they read the handler here and call {@link _runAuthRecovery}
362
+ * when they detect a transport-specific auth failure (XHR 401, WS close
363
+ * code 1008/4401, SSE pre-flight probe 401).
364
+ */
365
+ function _getAuthErrorHandler() {
366
+ return {
367
+ handler: authConfig?.onAuthError,
368
+ retryOn403: authConfig?.retryOn403 ?? false,
369
+ maxAuthRetries: Math.max(0, authConfig?.maxAuthRetries ?? 1)
370
+ };
371
+ }
372
+ /**
373
+ * @internal
374
+ * Drive the shared recovery cycle from a non-fetch transport. Same dedup
375
+ * as the fetch path — concurrent callers (XHR upload + WebSocket reconnect
376
+ * + SSE probe firing at once) collapse to one refresh.
377
+ */
378
+ function _runAuthRecovery(handler, ctx) {
379
+ return runAuthRecovery(handler, ctx);
380
+ }
381
+ /**
382
+ * @internal
383
+ * Resolve the next-attempt token. Mirrors the priority in `executeRequest`'s
384
+ * auth loop — `setToken` override beats re-reading `getToken()`. Exported so
385
+ * transports outside the fetch path apply the same precedence.
386
+ */
387
+ function _resolveRefreshedToken(overrideToken) {
388
+ if (overrideToken !== void 0) return overrideToken;
389
+ return readToken(authConfig?.getToken);
390
+ }
391
+ /**
392
+ * @internal
393
+ * True for any error a transport should run through `onAuthError`. Matches
394
+ * the fetch path's predicate so XHR / WS / SSE failures classify the same
395
+ * way (401, or 403 when `retryOn403`).
396
+ */
397
+ function _isAuthRecoverable(error, retryOn403) {
398
+ return isAuthRecoverable(error, retryOn403);
399
+ }
400
+ /** True for status codes that should trigger {@link AuthConfig.onAuthError}. */
401
+ function isAuthRecoverable(error, retryOn403) {
402
+ if (!isArcApiError(error)) return false;
403
+ if (error.status === 401) return true;
404
+ if (retryOn403 && error.status === 403) return true;
405
+ return false;
406
+ }
407
+ /**
408
+ * Run the recovery handler with concurrent-request dedup. Every concurrent
409
+ * 401 awaits the same promise and gets the same decision + override token.
410
+ * Cleared on settlement so a later (post-settlement) 401 starts a new cycle.
411
+ */
412
+ function runAuthRecovery(handler, ctx) {
413
+ if (pendingAuthRecovery) return pendingAuthRecovery;
414
+ pendingAuthRecovery = (async () => {
415
+ let overrideToken = void 0;
416
+ const setToken = (token) => {
417
+ overrideToken = token;
418
+ };
419
+ return {
420
+ decision: await handler({
421
+ ...ctx,
422
+ setToken
423
+ }),
424
+ overrideToken
425
+ };
426
+ })().finally(() => {
427
+ pendingAuthRecovery = null;
428
+ });
429
+ return pendingAuthRecovery;
430
+ }
431
+ /**
432
+ * Build an {@link AuthErrorHandler} from any `refresh()` function that
433
+ * returns the new token (or `null` if the session is truly expired).
434
+ *
435
+ * Catches refresh errors and surfaces them as `'skip'` by default so the
436
+ * original 401 reaches the consumer instead of a misleading "refresh failed"
437
+ * trace — consumers expect to handle "session expired" once, not twice. Pass
438
+ * `onRefreshError: 'throw'` to opt in to propagation.
439
+ *
440
+ * @example Better Auth (or any session-based lib)
441
+ * ```ts
442
+ * import { configureAuth, createAuthRefreshHandler } from '@classytic/arc-next/client';
443
+ * import { authClient } from '@/lib/auth-client';
444
+ *
445
+ * configureAuth({
446
+ * getToken: () => authClient.getSession().data?.session.token ?? null,
447
+ * onAuthError: createAuthRefreshHandler({
448
+ * refresh: async () => {
449
+ * const { data } = await authClient.getSession({ disableCookieCache: true });
450
+ * return data?.session.token ?? null;
451
+ * },
452
+ * }),
453
+ * });
454
+ * ```
455
+ *
456
+ * @example Custom OAuth refresh
457
+ * ```ts
458
+ * configureAuth({
459
+ * getToken: () => tokenStore.getAccessToken(),
460
+ * onAuthError: createAuthRefreshHandler({
461
+ * refresh: () => oauthClient.refresh(tokenStore.getRefreshToken()),
462
+ * }),
463
+ * });
464
+ * ```
465
+ */
466
+ function createAuthRefreshHandler(opts) {
467
+ return async ({ setToken }) => {
468
+ try {
469
+ const token = await opts.refresh();
470
+ if (token == null) return "skip";
471
+ setToken(token);
472
+ return "retry";
473
+ } catch (err) {
474
+ if (opts.onRefreshError === "throw") throw err;
475
+ return "skip";
476
+ }
477
+ };
478
+ }
479
+ /**
331
480
  * Build an auth-aware URL using the global client + auth singletons.
332
481
  *
333
482
  * Single source of truth for {@link import('./sse.js').buildSseUrl} (HTTP) and
@@ -436,16 +585,48 @@ function createClient(config) {
436
585
  * });
437
586
  */
438
587
  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
588
+ const { toast, navigation, getToken, getOrgId, headerName, ...overrideCfg } = overrides;
589
+ const authGetToken = getToken ?? (() => readToken(authConfig?.getToken));
590
+ const authGetOrgId = getOrgId ?? (() => authConfig?.getOrgId?.() ?? null);
591
+ const authHeaderName = headerName ?? authConfig?.headerName;
592
+ const clientAuth = {
593
+ getToken: authGetToken,
594
+ getOrgId: authGetOrgId,
595
+ headerName: authHeaderName
596
+ };
597
+ const resolveClientCfg = () => ({
598
+ baseUrl: overrideCfg.baseUrl ?? getBaseUrl(),
599
+ authMode: overrideCfg.authMode ?? getAuthMode(),
600
+ autoIdempotency: overrideCfg.autoIdempotency ?? isAutoIdempotency(),
601
+ elevated: overrideCfg.elevated ?? clientConfig?.elevated,
602
+ internalApiKey: overrideCfg.internalApiKey ?? clientConfig?.internalApiKey,
603
+ defaultHeaders: overrideCfg.defaultHeaders ?? clientConfig?.defaultHeaders,
604
+ credentials: overrideCfg.credentials ?? clientConfig?.credentials,
605
+ apiVersion: overrideCfg.apiVersion ?? clientConfig?.apiVersion,
606
+ retry: overrideCfg.retry ?? clientConfig?.retry,
607
+ beforeRequest: overrideCfg.beforeRequest ?? clientConfig?.beforeRequest,
608
+ afterResponse: overrideCfg.afterResponse ?? clientConfig?.afterResponse
448
609
  });
610
+ return {
611
+ request: (method, endpoint, options) => {
612
+ const cfg = resolveClientCfg();
613
+ const resolved = { ...options };
614
+ if (resolved.token === void 0) resolved.token = readToken(authGetToken);
615
+ if (resolved.organizationId === void 0) resolved.organizationId = authGetOrgId();
616
+ if (cfg.authMode === "header" && resolved.token) {
617
+ resolved.headerOptions = {
618
+ [authHeaderName ?? "x-api-key"]: resolved.token,
619
+ ...resolved.headerOptions ?? {}
620
+ };
621
+ resolved.token = void 0;
622
+ }
623
+ return executeRequest(cfg, method, endpoint, resolved);
624
+ },
625
+ config: resolveClientCfg(),
626
+ toast,
627
+ navigation,
628
+ auth: clientAuth
629
+ };
449
630
  }
450
631
  /**
451
632
  * Get auth context for a specific client instance, falling back to global.
@@ -470,7 +651,14 @@ function computeBackoff(retry, attempt) {
470
651
  if (strategy === "linear") return 300 * (attempt + 1);
471
652
  return Math.min(300 * Math.pow(2, attempt), 1e4);
472
653
  }
473
- async function executeRequest(config, method, endpoint, options = {}) {
654
+ /**
655
+ * Inner request loop — handles 5xx + network-failure backoff per
656
+ * {@link ClientConfig.retry}. Knows nothing about 401 recovery; that's the
657
+ * outer {@link executeRequest} wrapper. Split so the two retry families don't
658
+ * tangle: the backoff loop has its own attempt counter and predicate, the
659
+ * auth loop has its own cap and dedup.
660
+ */
661
+ async function executeWithBackoff(config, method, endpoint, options = {}) {
474
662
  const totalAttempts = Math.max(1, config.retry?.attempts ?? 1);
475
663
  const shouldRetry = (() => {
476
664
  const r = config.retry?.retryOn;
@@ -489,6 +677,47 @@ async function executeRequest(config, method, endpoint, options = {}) {
489
677
  }
490
678
  throw lastError;
491
679
  }
680
+ /**
681
+ * Top-level request entry. Wraps the 5xx-backoff loop with the 401-recovery
682
+ * loop so the two retry families compose cleanly:
683
+ *
684
+ * 401 (auth) ─→ onAuthError ─→ retry with fresh token ─→ may 5xx ─→ backoff
685
+ *
686
+ * `maxAuthRetries` (default 1) caps the auth loop independently of the
687
+ * backoff loop's `attempts`. AbortSignal propagates through both loops.
688
+ *
689
+ * The auth loop only fires when {@link AuthConfig.onAuthError} is configured —
690
+ * apps that haven't wired a refresh handler see the original behavior (401
691
+ * surfaces immediately, no extra round-trip).
692
+ */
693
+ async function executeRequest(config, method, endpoint, options = {}) {
694
+ const handler = authConfig?.onAuthError;
695
+ if (!handler) return executeWithBackoff(config, method, endpoint, options);
696
+ const retryOn403 = authConfig?.retryOn403 ?? false;
697
+ const maxAuthRetries = Math.max(0, authConfig?.maxAuthRetries ?? 1);
698
+ let currentOptions = options;
699
+ for (let authAttempt = 0; authAttempt <= maxAuthRetries; authAttempt++) try {
700
+ return await executeWithBackoff(config, method, endpoint, currentOptions);
701
+ } catch (error) {
702
+ if (authAttempt >= maxAuthRetries || !isAuthRecoverable(error, retryOn403)) throw error;
703
+ if (currentOptions.signal?.aborted) throw error;
704
+ const { decision, overrideToken } = await runAuthRecovery(handler, {
705
+ error,
706
+ request: {
707
+ method,
708
+ endpoint
709
+ },
710
+ attempt: authAttempt + 1
711
+ });
712
+ if (decision !== "retry") throw error;
713
+ const nextToken = overrideToken !== void 0 ? overrideToken : readToken(authConfig?.getToken);
714
+ currentOptions = {
715
+ ...currentOptions,
716
+ token: nextToken
717
+ };
718
+ }
719
+ throw new Error("arc-next: auth retry loop terminated without resolution");
720
+ }
492
721
  /** Sleep that resolves early if the signal aborts. */
493
722
  function sleepAbortable(ms, signal) {
494
723
  return new Promise((resolve, reject) => {
@@ -517,21 +746,23 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
517
746
  ...config.defaultHeaders ?? {}
518
747
  };
519
748
  if (config.internalApiKey) headers["x-internal-api-key"] = config.internalApiKey;
520
- if (token) if (config.authMode === "header") {
521
- const headerName = authConfig?.headerName ?? "x-api-key";
522
- headers[headerName] = token;
523
- } else headers["Authorization"] = `Bearer ${token}`;
749
+ if (token) {
750
+ if (config.authMode === "header") {
751
+ const headerName = authConfig?.headerName ?? "x-api-key";
752
+ headers[headerName] = token;
753
+ } else if (config.authMode !== "cookie") headers["Authorization"] = `Bearer ${token}`;
754
+ }
524
755
  if (config.apiVersion) headers["Accept-Version"] = config.apiVersion;
525
756
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
526
757
  if (elevated ?? config.elevated ?? false) headers["x-arc-scope"] = "platform";
527
- if (body !== void 0 && body !== null && !(body instanceof FormData)) headers["Content-Type"] = "application/json";
758
+ if (body !== void 0 && body !== null && !isNonJsonBody(body)) headers["Content-Type"] = "application/json";
528
759
  if (headerOptions) headers = {
529
760
  ...headers,
530
761
  ...headerOptions
531
762
  };
532
763
  const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
533
764
  let serializedBody = void 0;
534
- if (body !== void 0 && body !== null) serializedBody = body instanceof FormData ? body : JSON.stringify(body);
765
+ if (body !== void 0 && body !== null) serializedBody = isNonJsonBody(body) ? body : JSON.stringify(body);
535
766
  if (config.beforeRequest) {
536
767
  const ctx = await config.beforeRequest({
537
768
  method,
@@ -560,6 +791,7 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
560
791
  ...fetchOptions.next,
561
792
  tags
562
793
  };
794
+ if (!/^https?:\/\//i.test(endpoint) && !config.baseUrl) throw new Error(`[arc-next] handleApiRequest(${method} ${endpoint}): baseUrl is empty. Call configureClient({ baseUrl: '...' }) BEFORE the first request. If you use createAuthAwareClient() at module top-level, make sure the Providers component runs configureClient() first (e.g. in a useState() initializer, before the children render).`);
563
795
  const response = await fetch(`${config.baseUrl}${endpoint}`, fetchOptions);
564
796
  if (!response.ok) {
565
797
  let json = null;
@@ -692,6 +924,183 @@ function createQueryString(params = {}) {
692
924
  });
693
925
  return searchParams.toString();
694
926
  }
927
+ /**
928
+ * Body shapes that carry their own Content-Type and must NOT be re-serialized
929
+ * by arc-next:
930
+ *
931
+ * - `FormData` — multipart with a runtime-computed boundary.
932
+ * - `Blob` / `File` — carries `.type`.
933
+ * - `URLSearchParams` — `application/x-www-form-urlencoded`.
934
+ * - `ArrayBuffer` / typed arrays — raw bytes; caller controls Content-Type.
935
+ * - `ReadableStream` — caller controls Content-Type.
936
+ * - `string` — caller controls Content-Type (could be plain text, XML, etc.).
937
+ *
938
+ * Plain objects and arrays fall through and get `JSON.stringify`d with
939
+ * `Content-Type: application/json`.
940
+ */
941
+ function isNonJsonBody(body) {
942
+ if (body == null) return false;
943
+ if (typeof body === "string") return true;
944
+ if (typeof FormData !== "undefined" && body instanceof FormData) return true;
945
+ if (typeof Blob !== "undefined" && body instanceof Blob) return true;
946
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return true;
947
+ if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) return true;
948
+ if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(body)) return true;
949
+ if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) return true;
950
+ return false;
951
+ }
952
+ /**
953
+ * Returns the auth headers arc-next would inject on a fetch right now —
954
+ * `Authorization` (or the configured custom `headerName` for `authMode:
955
+ * 'header'`), `x-organization-id`, and `x-internal-api-key`. Use for the
956
+ * rare case where you need full `Response` control via plain `fetch` but
957
+ * still want arc-next's auth wiring.
958
+ *
959
+ * @example
960
+ * const res = await fetch(url, {
961
+ * headers: { ...arcAuthHeaders(), 'X-Custom': '1' },
962
+ * credentials: getAuthMode() === 'cookie' ? 'include' : 'same-origin',
963
+ * });
964
+ */
965
+ function arcAuthHeaders() {
966
+ const { token, organizationId } = getAuthContext();
967
+ const authMode = getAuthMode();
968
+ const headers = {};
969
+ if (token) {
970
+ if (authMode === "header") headers[authConfig?.headerName ?? "x-api-key"] = token;
971
+ else if (authMode !== "cookie") headers.Authorization = `Bearer ${token}`;
972
+ }
973
+ if (organizationId) headers["x-organization-id"] = organizationId;
974
+ if (clientConfig?.internalApiKey) headers["x-internal-api-key"] = clientConfig.internalApiKey;
975
+ return headers;
976
+ }
977
+ /**
978
+ * Headers that arc-next owns and a caller must not override via
979
+ * {@link ArcFetchOptions.headers}. Passing one of these in `options.headers`
980
+ * is silently dropped — see the `arcFetch` JSDoc for the rationale.
981
+ *
982
+ * Lower-cased on lookup so case-insensitive HTTP header semantics are
983
+ * honored (`Authorization` vs `authorization` both protected).
984
+ */
985
+ const ARC_FETCH_PROTECTED_HEADERS = new Set([
986
+ "authorization",
987
+ "x-organization-id",
988
+ "x-internal-api-key"
989
+ ]);
990
+ let defaultArcFetchClient = null;
991
+ function getDefaultArcFetchClient() {
992
+ if (!defaultArcFetchClient) defaultArcFetchClient = createAuthAwareClient();
993
+ return defaultArcFetchClient;
994
+ }
995
+ /** @internal — tests reset the default client between cases. */
996
+ function _resetArcFetchClient() {
997
+ defaultArcFetchClient = null;
998
+ }
999
+ /**
1000
+ * Strip protected auth headers from a caller-supplied map. Case-insensitive
1001
+ * (HTTP header names are case-insensitive). The custom-auth `headerName`
1002
+ * (when `authMode: 'header'` is configured) is computed at call time so
1003
+ * apps that reconfigure auth modes don't lose protection on the renamed
1004
+ * header.
1005
+ */
1006
+ function sanitizeUserHeaders(headers) {
1007
+ if (!headers) return {};
1008
+ const customHeader = authConfig?.headerName?.toLowerCase();
1009
+ const out = {};
1010
+ for (const [name, value] of Object.entries(headers)) {
1011
+ const lower = name.toLowerCase();
1012
+ if (ARC_FETCH_PROTECTED_HEADERS.has(lower)) continue;
1013
+ if (customHeader && lower === customHeader) continue;
1014
+ out[name] = value;
1015
+ }
1016
+ return out;
1017
+ }
1018
+ /**
1019
+ * Authenticated, tenant-scoped fetch to an arc endpoint — one line for the
1020
+ * non-hook contexts where `useQuery` / `useMutation` aren't available
1021
+ * (event handlers, service workers, server actions, custom MDX submits,
1022
+ * background polls).
1023
+ *
1024
+ * Auto-injects on every call:
1025
+ * - `Authorization: Bearer <token>` (from `configureAuth().getToken`), or
1026
+ * the custom header for `authMode: 'header'`
1027
+ * - `x-organization-id` (from `configureAuth().getOrgId`)
1028
+ * - `Content-Type: application/json` (only for plain object/array bodies)
1029
+ * - `x-internal-api-key`, `Accept-Version`, `Idempotency-Key`,
1030
+ * `x-arc-scope` when configured
1031
+ *
1032
+ * Composes with everything else `configureClient` + `configureAuth` do:
1033
+ * - `retry` (5xx backoff)
1034
+ * - `onAuthError` (401 → refresh → retry, with concurrent dedup)
1035
+ * - `beforeRequest` / `afterResponse` interceptors
1036
+ * - `cookie` / `bearer` / `header` auth modes
1037
+ *
1038
+ * Response handling:
1039
+ * - 2xx → parsed body (JSON for `application/json`, Blob for binary,
1040
+ * text for `text/*`).
1041
+ * - non-2xx → throws `ArcApiError` with parsed body, status, endpoint,
1042
+ * method. Use `isArcApiError(err)` + `err.code` to discriminate.
1043
+ *
1044
+ * For full `Response` control (rare), use plain `fetch` with
1045
+ * {@link arcAuthHeaders} instead.
1046
+ *
1047
+ * @example
1048
+ * import { arc } from '@classytic/arc-next/client';
1049
+ *
1050
+ * // Before — 15 lines of header dance + error parse + JSON
1051
+ * // After:
1052
+ * const result = await arc.post<{ ok: true }>('/api/statements', statements);
1053
+ */
1054
+ function arcFetch(path, options = {}) {
1055
+ const { method = "GET", body, headers, signal, elevated, idempotencyKey, revalidate, tags, cache, client } = options;
1056
+ const transport = client ?? getDefaultArcFetchClient();
1057
+ const apiOptions = {
1058
+ body,
1059
+ headerOptions: sanitizeUserHeaders(headers),
1060
+ ...signal ? { signal } : {},
1061
+ ...elevated !== void 0 ? { elevated } : {},
1062
+ ...idempotencyKey ? { idempotencyKey } : {},
1063
+ ...revalidate !== void 0 ? { revalidate } : {},
1064
+ ...tags ? { tags } : {},
1065
+ ...cache ? { cache } : {}
1066
+ };
1067
+ return transport.request(method, path, apiOptions);
1068
+ }
1069
+ /**
1070
+ * Method-specific shorthands for the 90% case. Each mirrors `arcFetch` with
1071
+ * the HTTP verb pre-filled; mutating verbs accept `body` as the second arg
1072
+ * so the call reads as a sentence:
1073
+ *
1074
+ * `arc.post('/path', payload)` instead of
1075
+ * `arcFetch('/path', { method: 'POST', body: payload })`.
1076
+ *
1077
+ * Identical composition with `onAuthError`, retry, and interceptors.
1078
+ */
1079
+ const arc = {
1080
+ get: (path, opts = {}) => arcFetch(path, {
1081
+ ...opts,
1082
+ method: "GET"
1083
+ }),
1084
+ post: (path, body, opts = {}) => arcFetch(path, {
1085
+ ...opts,
1086
+ method: "POST",
1087
+ body
1088
+ }),
1089
+ put: (path, body, opts = {}) => arcFetch(path, {
1090
+ ...opts,
1091
+ method: "PUT",
1092
+ body
1093
+ }),
1094
+ patch: (path, body, opts = {}) => arcFetch(path, {
1095
+ ...opts,
1096
+ method: "PATCH",
1097
+ body
1098
+ }),
1099
+ delete: (path, opts = {}) => arcFetch(path, {
1100
+ ...opts,
1101
+ method: "DELETE"
1102
+ })
1103
+ };
695
1104
 
696
1105
  //#endregion
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 };
1106
+ export { ArcApiError, KNOWN_ARC_ERROR_CODES, _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 };