@classytic/arc-next 0.12.0 → 0.14.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Classytic
3
+ Copyright (c) 2025 Classytic LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -650,3 +650,10 @@ const { mutateAsync: publish, isPending } = useMutationWithTransition({
650
650
  ## License
651
651
 
652
652
  MIT
653
+
654
+
655
+ ## Trademark
656
+
657
+ The code is MIT-licensed. **"Classytic", "arc", and the logos are trademarks of
658
+ Classytic LLC** and are **not** licensed under MIT — see [TRADEMARK.md](TRADEMARK.md).
659
+ Forks must be renamed; the license covers the code, not the brand.
package/dist/api.d.ts CHANGED
@@ -2,7 +2,6 @@ import { ArcClient, NextFetchOptions } from "./client.js";
2
2
  import { BracketOperator, BracketOperator as BracketOperator$1, ParsedPopulate } from "@classytic/repo-core/query-parser";
3
3
  import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, PaginatedResult, SortDirection as SortDirection$1 } from "@classytic/repo-core/pagination";
4
4
  import { AggResult, AggRow, BulkCreateResult, DeleteManyResult, DeleteResult, UpdateManyResult } from "@classytic/repo-core/repository";
5
-
6
5
  //#region src/api.d.ts
7
6
  /**
8
7
  * URL-emittable subset of repo-core's canonical {@link ParsedPopulate} —
@@ -100,7 +99,18 @@ interface BaseApiConfig {
100
99
  };
101
100
  cache?: RequestCache;
102
101
  headers?: Record<string, string>;
103
- client?: ArcClient;
102
+ /**
103
+ * Transport for this API instance.
104
+ *
105
+ * - `ArcClient` — RETAINED: the instance is captured and every request goes
106
+ * through it (isolated multi-client setups, tests, background jobs).
107
+ * - `() => ArcClient` — PROVIDER: re-resolved on EVERY request, so a
108
+ * consumer SDK can swap/reconfigure the underlying client after APIs are
109
+ * constructed without Proxy tricks or stale-capture bugs (the reason the
110
+ * commerce SDK previously wrapped its default client in a `Proxy`).
111
+ * - omitted — the module-global transport (`configureClient`/`configureAuth`).
112
+ */
113
+ client?: ArcClient | (() => ArcClient);
104
114
  }
105
115
  declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
106
116
  readonly entity: string;
@@ -129,12 +139,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
129
139
  private withCacheDefault;
130
140
  createQueryString(params?: Record<string, unknown>): string;
131
141
  prepareParams(params?: QueryParams): Record<string, unknown>;
132
- getAll({
133
- token,
134
- organizationId,
135
- params,
136
- options
137
- }?: {
142
+ getAll({ token, organizationId, params, options }?: {
138
143
  token?: string | null;
139
144
  organizationId?: string | null;
140
145
  params?: QueryParams;
@@ -145,37 +150,21 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
145
150
  * (`?_count=true`): same permissions/row-filters/tenant scoping as
146
151
  * `getAll`, ZERO documents fetched. Cheapest way to answer "how many".
147
152
  */
148
- count({
149
- token,
150
- organizationId,
151
- params,
152
- options
153
- }?: {
153
+ count({ token, organizationId, params, options }?: {
154
154
  token?: string | null;
155
155
  organizationId?: string | null;
156
156
  params?: QueryParams;
157
157
  options?: Omit<RequestOptions, "token" | "organizationId">;
158
158
  }): Promise<number>;
159
159
  /** Whether ANY record matches the filters (`?_exists=true`). */
160
- exists({
161
- token,
162
- organizationId,
163
- params,
164
- options
165
- }?: {
160
+ exists({ token, organizationId, params, options }?: {
166
161
  token?: string | null;
167
162
  organizationId?: string | null;
168
163
  params?: QueryParams;
169
164
  options?: Omit<RequestOptions, "token" | "organizationId">;
170
165
  }): Promise<boolean>;
171
166
  /** Distinct values of a field across matching records (`?_distinct=field`). */
172
- distinct<TValue = unknown>({
173
- token,
174
- organizationId,
175
- field,
176
- params,
177
- options
178
- }: {
167
+ distinct<TValue = unknown>({ token, organizationId, field, params, options }: {
179
168
  token?: string | null;
180
169
  organizationId?: string | null;
181
170
  field: string;
@@ -184,13 +173,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
184
173
  }): Promise<TValue[]>;
185
174
  /** Shared raw GET against the list route (dispatch verbs bypass pagination parsing). */
186
175
  private getAllRaw;
187
- getById({
188
- token,
189
- organizationId,
190
- id,
191
- params,
192
- options
193
- }: {
176
+ getById({ token, organizationId, id, params, options }: {
194
177
  token?: string | null;
195
178
  organizationId?: string | null;
196
179
  id: string;
@@ -200,60 +183,33 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
200
183
  };
201
184
  options?: Omit<RequestOptions, "token" | "organizationId">;
202
185
  }): Promise<TDoc>;
203
- create({
204
- token,
205
- organizationId,
206
- data,
207
- options
208
- }: {
186
+ create({ token, organizationId, data, options }: {
209
187
  token?: string | null;
210
188
  organizationId?: string | null;
211
189
  data: TCreate;
212
190
  options?: Omit<RequestOptions, "token" | "organizationId">;
213
191
  }): Promise<TDoc>;
214
- update({
215
- token,
216
- organizationId,
217
- id,
218
- data,
219
- options
220
- }: {
192
+ update({ token, organizationId, id, data, options }: {
221
193
  token?: string | null;
222
194
  organizationId?: string | null;
223
195
  id: string;
224
196
  data: TUpdate;
225
197
  options?: Omit<RequestOptions, "token" | "organizationId">;
226
198
  }): Promise<TDoc>;
227
- delete({
228
- token,
229
- organizationId,
230
- id,
231
- options
232
- }: {
199
+ delete({ token, organizationId, id, options }: {
233
200
  token?: string | null;
234
201
  organizationId?: string | null;
235
202
  id: string;
236
203
  options?: Omit<RequestOptions, "token" | "organizationId">;
237
204
  }): Promise<DeleteResult>;
238
- upload({
239
- token,
240
- organizationId,
241
- data,
242
- id,
243
- path,
244
- options
245
- }: ScopedArgs & {
246
- data: FormData; /** Resource ID — shorthand for path, appended as `baseUrl/{id}/upload` */
247
- id?: string; /** Custom sub-path appended as `baseUrl/{path}`. Takes precedence over `id`. */
205
+ upload({ token, organizationId, data, id, path, options }: ScopedArgs & {
206
+ data: FormData;
207
+ /** Resource ID — shorthand for path, appended as `baseUrl/{id}/upload` */
208
+ id?: string;
209
+ /** Custom sub-path appended as `baseUrl/{path}`. Takes precedence over `id`. */
248
210
  path?: string;
249
211
  }): Promise<TDoc>;
250
- request<TResponse = unknown>(method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE", endpoint: string, {
251
- token,
252
- organizationId,
253
- data,
254
- params,
255
- options
256
- }?: ScopedArgs & {
212
+ request<TResponse = unknown>(method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE", endpoint: string, { token, organizationId, data, params, options }?: ScopedArgs & {
257
213
  data?: unknown;
258
214
  params?: QueryParams;
259
215
  }): Promise<TResponse>;
@@ -290,16 +246,9 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
290
246
  * data: { source: 'csv', items: [...] },
291
247
  * });
292
248
  */
293
- invokeRoute<TResponse = unknown>({
294
- token,
295
- organizationId,
296
- method,
297
- path,
298
- data,
299
- params,
300
- options
301
- }: ScopedArgs & {
302
- method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; /** Path relative to the resource baseUrl. Leading slash optional. */
249
+ invokeRoute<TResponse = unknown>({ token, organizationId, method, path, data, params, options }: ScopedArgs & {
250
+ method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
251
+ /** Path relative to the resource baseUrl. Leading slash optional. */
303
252
  path: string;
304
253
  data?: unknown;
305
254
  params?: QueryParams;
@@ -313,14 +262,9 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
313
262
  * filter: { from: '2025-01-01', to: '2025-12-31' },
314
263
  * });
315
264
  */
316
- aggregate<TRow extends AggRow = AggRow>({
317
- token,
318
- organizationId,
319
- name,
320
- filter,
321
- options
322
- }: ScopedArgs & {
323
- /** Aggregation name as declared on the resource. */name: string;
265
+ aggregate<TRow extends AggRow = AggRow>({ token, organizationId, name, filter, options }: ScopedArgs & {
266
+ /** Aggregation name as declared on the resource. */
267
+ name: string;
324
268
  /**
325
269
  * URL-encoded filter narrows + dimension args. Reserved keys (`page`,
326
270
  * `limit`, etc.) are stripped server-side; everything else flows into
@@ -328,14 +272,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
328
272
  */
329
273
  filter?: Record<string, unknown>;
330
274
  }): Promise<AggResult<TRow>>;
331
- dispatchAction<TResult = unknown, TBody extends Record<string, unknown> = Record<string, unknown>>({
332
- token,
333
- organizationId,
334
- id,
335
- action,
336
- data,
337
- options
338
- }: ScopedArgs & {
275
+ dispatchAction<TResult = unknown, TBody extends Record<string, unknown> = Record<string, unknown>>({ token, organizationId, id, action, data, options }: ScopedArgs & {
339
276
  id: string;
340
277
  action: string;
341
278
  data?: TBody;
package/dist/api.js CHANGED
@@ -14,7 +14,8 @@ var BaseApi = class {
14
14
  requestFn;
15
15
  constructor(entity, config = {}) {
16
16
  this.entity = entity;
17
- this.requestFn = config.client?.request ?? handleApiRequest;
17
+ const client = config.client;
18
+ this.requestFn = typeof client === "function" ? (method, endpoint, options) => client().request(method, endpoint, options) : client?.request ?? handleApiRequest;
18
19
  this.config = {
19
20
  basePath: config.basePath ?? "/api/v1",
20
21
  defaultParams: {
@@ -97,12 +98,14 @@ var BaseApi = class {
97
98
  });
98
99
  return;
99
100
  }
100
- if (value !== void 0 && value !== "") if (["page", "limit"].includes(key)) result[key] = parseInt(String(value), 10) || (key === "page" ? 1 : 10);
101
- else if (Array.isArray(value)) {
102
- if (/\[([^\]]+)\]$/.test(key)) result[key] = value.join(",");
103
- else if (value.length > 1) result[`${key}[in]`] = value.join(",");
104
- else if (value.length === 1) result[key] = value[0];
105
- } else result[key] = value;
101
+ if (value !== void 0 && value !== "") {
102
+ if (["page", "limit"].includes(key)) result[key] = parseInt(String(value), 10) || (key === "page" ? 1 : 10);
103
+ else if (Array.isArray(value)) {
104
+ if (/\[([^\]]+)\]$/.test(key)) result[key] = value.join(",");
105
+ else if (value.length > 1) result[`${key}[in]`] = value.join(",");
106
+ else if (value.length === 1) result[key] = value[0];
107
+ } else result[key] = value;
108
+ }
106
109
  });
107
110
  return result;
108
111
  }
package/dist/cache.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { QueryClient, QueryKey } from "@tanstack/react-query";
2
-
3
2
  //#region src/cache.d.ts
4
3
  interface PaginationData {
5
4
  /** Pagination method detected from response (offset | keyset | aggregate) */
@@ -21,16 +20,20 @@ declare const DEFAULT_QUERY_CONFIG: {
21
20
  };
22
21
  /** Pre-built query config presets for common data freshness patterns. */
23
22
  declare const QUERY_CONFIGS: {
24
- /** Live data: 20s stale, 30s polling */readonly realtime: {
23
+ /** Live data: 20s stale, 30s polling */
24
+ readonly realtime: {
25
25
  readonly staleTime: 20000;
26
26
  readonly refetchInterval: 30000;
27
- }; /** Frequently updated: 60s stale */
27
+ };
28
+ /** Frequently updated: 60s stale */
28
29
  readonly frequent: {
29
30
  readonly staleTime: 60000;
30
- }; /** Stable data: 5min stale (same as default) */
31
+ };
32
+ /** Stable data: 5min stale (same as default) */
31
33
  readonly stable: {
32
34
  readonly staleTime: 300000;
33
- }; /** Rarely changes: 10min stale */
35
+ };
36
+ /** Rarely changes: 10min stale */
34
37
  readonly static: {
35
38
  readonly staleTime: 600000;
36
39
  };
package/dist/cache.js CHANGED
@@ -1,7 +1,7 @@
1
1
  //#region src/cache.ts
2
2
  const DEFAULT_QUERY_CONFIG = {
3
- staleTime: 300 * 1e3,
4
- gcTime: 1800 * 1e3,
3
+ staleTime: 3e5,
4
+ gcTime: 18e5,
5
5
  refetchOnWindowFocus: false,
6
6
  retry: 0
7
7
  };
package/dist/client.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { ErrorDetail } from "@classytic/repo-core/errors";
2
-
3
2
  //#region src/client.d.ts
4
3
  interface ToastHandler {
5
4
  success: (message: string) => void;
@@ -34,7 +33,7 @@ type UseRouterHook = () => {
34
33
  * `(string & {})` keeps the type open so domain packages and custom
35
34
  * `errorMappers` codes still satisfy it.
36
35
  */
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"];
36
+ 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.tier_required", "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"];
38
37
  /**
39
38
  * Canonical arc error code union. `(string & {})` keeps the type open so
40
39
  * domain packages can extend hierarchically (`'order.cart.locked'`,
@@ -194,6 +193,43 @@ declare function getQuotaDetails(error: unknown): QuotaDetails | null;
194
193
  * }
195
194
  */
196
195
  declare function isOrgContextRequiredError(error: unknown): error is ArcApiError;
196
+ /** The tier/mode a `arc.tier_required` error asked for, plus the active one. */
197
+ interface TierRequirement {
198
+ /** Minimum tier/mode the feature needs (e.g. `"enterprise"`). */
199
+ requiredMode: string;
200
+ /** The deployment's current tier/mode (e.g. `"standard"`). */
201
+ currentMode?: string;
202
+ }
203
+ /**
204
+ * TIER/CAPABILITY gate predicate. True when the backend refused because the
205
+ * deployment's tier (FLOW_MODE / feature tier) is below what the route needs —
206
+ * a `arc.tier_required` error (arc-inventory's mode gate → arc's
207
+ * `createDomainError`). This is the DISCRIMINABLE counterpart to a bare
208
+ * `arc.forbidden` (a role/permission denial): switch on THIS to render an
209
+ * "upgrade required / requires Enterprise" surface, and on `arc.forbidden` for
210
+ * "access denied". The required tier is available via {@link getTierRequirement}.
211
+ *
212
+ * @example
213
+ * if (isTierRequiredError(error)) {
214
+ * const { requiredMode } = getTierRequirement(error)!;
215
+ * showUpgradePanel(requiredMode); // "requires Enterprise"
216
+ * } else if (isArcErrorCode(error, "arc.forbidden")) {
217
+ * showAccessDenied(); // role/branch problem
218
+ * }
219
+ */
220
+ declare function isTierRequiredError(error: unknown): error is ArcApiError;
221
+ /**
222
+ * Extract the tier requirement from a `arc.tier_required` error's structured
223
+ * machine-readable context (`{ requiredMode, currentMode }`). Returns `null` for
224
+ * any other error, so the FE never has to hardcode a route→tier map or
225
+ * string-match a message.
226
+ *
227
+ * Reads `meta` (the canonical ErrorContract Record — where both arc gate paths
228
+ * put it: the global error handler serializes `ArcError.meta`, and the
229
+ * permission-slot applier emits the same `meta`). Falls back to `details` for
230
+ * resilience against any backend still emitting the pre-2026-07 shape.
231
+ */
232
+ declare function getTierRequirement(error: unknown): TierRequirement | null;
197
233
  /**
198
234
  * Specific predicate for validation failures (Fastify AJV + Mongoose
199
235
  * ValidationError). When true, `error.fieldErrors` is populated with the
@@ -267,6 +303,13 @@ interface ClientConfig {
267
303
  * @example '2' // sends Accept-Version: 2
268
304
  */
269
305
  apiVersion?: string;
306
+ /**
307
+ * Default request cache mode applied to every request that expresses no
308
+ * caching intent of its own (no per-call `cache`, `revalidate`, or `next`) —
309
+ * the client-level analog of `BaseApiConfig.cache`, same precedence rule.
310
+ * Unset = the runtime's fetch default.
311
+ */
312
+ cache?: RequestCache;
270
313
  /**
271
314
  * Auto-generate `Idempotency-Key` header for POST/PUT/PATCH requests.
272
315
  * Prevents duplicate mutations on network retries.
@@ -973,4 +1016,4 @@ declare const arc: {
973
1016
  delete: <T = unknown>(path: string, opts?: ArcFetchOptions) => Promise<T>;
974
1017
  };
975
1018
  //#endregion
976
- export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcErrorCode, ArcFetchOptions, AuthConfig, AuthErrorContext, AuthErrorHandler, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, ClientEncryptionConfig, HttpMethod, KNOWN_ARC_ERROR_CODES, NextFetchOptions, QuotaDetails, RetryConfig, ServerClientConfig, StreamUrlProtocol, TextResponse, ToastHandler, UseRouterHook, _getAuthErrorHandler, _isAuthRecoverable, _resetArcFetchClient, _resetAuthRecovery, _resetAuthWarnings, _resolveRefreshedToken, _runAuthRecovery, arc, arcAuthHeaders, arcFetch, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createAuthRefreshHandler, createClient, createQueryString, createServerClient, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, getQuotaDetails, handleApiRequest, hasGlobalStaticAuth, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isQuotaExceeded, isValidationError };
1019
+ export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcErrorCode, ArcFetchOptions, AuthConfig, AuthErrorContext, AuthErrorHandler, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, ClientEncryptionConfig, HttpMethod, KNOWN_ARC_ERROR_CODES, NextFetchOptions, QuotaDetails, RetryConfig, ServerClientConfig, StreamUrlProtocol, TextResponse, TierRequirement, ToastHandler, UseRouterHook, _getAuthErrorHandler, _isAuthRecoverable, _resetArcFetchClient, _resetAuthRecovery, _resetAuthWarnings, _resolveRefreshedToken, _runAuthRecovery, arc, arcAuthHeaders, arcFetch, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createAuthRefreshHandler, createClient, createQueryString, createServerClient, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, getQuotaDetails, getTierRequirement, handleApiRequest, hasGlobalStaticAuth, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isQuotaExceeded, isTierRequiredError, isValidationError };
package/dist/client.js CHANGED
@@ -37,6 +37,7 @@ const KNOWN_ARC_ERROR_CODES = [
37
37
  "arc.gateway_timeout",
38
38
  "arc.validation_error",
39
39
  "arc.invalid_id",
40
+ "arc.tier_required",
40
41
  "arc.org.selection_required",
41
42
  "arc.org.access_denied",
42
43
  "ORG_CONTEXT_REQUIRED",
@@ -243,6 +244,48 @@ function isOrgContextRequiredError(error) {
243
244
  return isArcErrorCode(error, "ORG_CONTEXT_REQUIRED");
244
245
  }
245
246
  /**
247
+ * TIER/CAPABILITY gate predicate. True when the backend refused because the
248
+ * deployment's tier (FLOW_MODE / feature tier) is below what the route needs —
249
+ * a `arc.tier_required` error (arc-inventory's mode gate → arc's
250
+ * `createDomainError`). This is the DISCRIMINABLE counterpart to a bare
251
+ * `arc.forbidden` (a role/permission denial): switch on THIS to render an
252
+ * "upgrade required / requires Enterprise" surface, and on `arc.forbidden` for
253
+ * "access denied". The required tier is available via {@link getTierRequirement}.
254
+ *
255
+ * @example
256
+ * if (isTierRequiredError(error)) {
257
+ * const { requiredMode } = getTierRequirement(error)!;
258
+ * showUpgradePanel(requiredMode); // "requires Enterprise"
259
+ * } else if (isArcErrorCode(error, "arc.forbidden")) {
260
+ * showAccessDenied(); // role/branch problem
261
+ * }
262
+ */
263
+ function isTierRequiredError(error) {
264
+ return isArcErrorCode(error, "arc.tier_required");
265
+ }
266
+ /**
267
+ * Extract the tier requirement from a `arc.tier_required` error's structured
268
+ * machine-readable context (`{ requiredMode, currentMode }`). Returns `null` for
269
+ * any other error, so the FE never has to hardcode a route→tier map or
270
+ * string-match a message.
271
+ *
272
+ * Reads `meta` (the canonical ErrorContract Record — where both arc gate paths
273
+ * put it: the global error handler serializes `ArcError.meta`, and the
274
+ * permission-slot applier emits the same `meta`). Falls back to `details` for
275
+ * resilience against any backend still emitting the pre-2026-07 shape.
276
+ */
277
+ function getTierRequirement(error) {
278
+ if (!isTierRequiredError(error)) return null;
279
+ const j = error.json ?? {};
280
+ const bag = j.meta ?? j.details ?? {};
281
+ const requiredMode = bag.requiredMode;
282
+ if (typeof requiredMode !== "string") return null;
283
+ return {
284
+ requiredMode,
285
+ ...typeof bag.currentMode === "string" ? { currentMode: bag.currentMode } : {}
286
+ };
287
+ }
288
+ /**
246
289
  * Specific predicate for validation failures (Fastify AJV + Mongoose
247
290
  * ValidationError). When true, `error.fieldErrors` is populated with the
248
291
  * `{ field: message }` map. Matches arc's `arc.validation_error` and the
@@ -756,6 +799,10 @@ async function executeRequest(config, method, endpoint, options = {}) {
756
799
  ...options,
757
800
  idempotencyKey: globalThis.crypto.randomUUID()
758
801
  };
802
+ if (config.cache !== void 0 && options.cache === void 0 && options.revalidate === void 0 && options.next === void 0) options = {
803
+ ...options,
804
+ cache: config.cache
805
+ };
759
806
  const handler = authConfig?.onAuthError;
760
807
  if (!handler) return executeWithBackoff(config, method, endpoint, options);
761
808
  const retryOn403 = authConfig?.retryOn403 ?? false;
@@ -825,8 +872,10 @@ function withTimeoutSignal(signal, timeoutMs) {
825
872
  const controller = new AbortController();
826
873
  let timedOut = false;
827
874
  const onCallerAbort = () => controller.abort();
828
- if (signal) if (signal.aborted) controller.abort();
829
- else signal.addEventListener("abort", onCallerAbort, { once: true });
875
+ if (signal) {
876
+ if (signal.aborted) controller.abort();
877
+ else signal.addEventListener("abort", onCallerAbort, { once: true });
878
+ }
830
879
  const timer = setTimeout(() => {
831
880
  timedOut = true;
832
881
  controller.abort();
@@ -899,7 +948,7 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
899
948
  if (revalidate !== void 0) nextCfg.revalidate = revalidate;
900
949
  if (tags) nextCfg.tags = nextCfg.tags ? [...nextCfg.tags, ...tags] : tags;
901
950
  if (nextCfg.revalidate !== void 0 || nextCfg.tags) fetchOptions.next = nextCfg;
902
- 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).`);
951
+ if (!/^https?:\/\//i.test(endpoint) && !config.baseUrl) throw new Error(`[arc-next] handleApiRequest(${method} ${endpoint}): baseUrl is empty. ` + (typeof window === "undefined" ? "On the server, configureClient() is a no-op — configure a request-scoped client instead: createServerClient({ baseUrl }), or your SDK's server config (e.g. a server env baseUrl / runWithSDKConfig) so SSR reads resolve." : "Call configureClient({ baseUrl: '...' }) at app boot (Providers) BEFORE the first request — e.g. in a useState() initializer, before the children render."));
903
952
  const response = await fetch(`${config.baseUrl}${endpoint}`, fetchOptions);
904
953
  if (!response.ok) {
905
954
  let json = null;
@@ -1105,7 +1154,7 @@ function arcAuthHeaders() {
1105
1154
  * Lower-cased on lookup so case-insensitive HTTP header semantics are
1106
1155
  * honored (`Authorization` vs `authorization` both protected).
1107
1156
  */
1108
- const ARC_FETCH_PROTECTED_HEADERS = new Set([
1157
+ const ARC_FETCH_PROTECTED_HEADERS = /* @__PURE__ */ new Set([
1109
1158
  "authorization",
1110
1159
  "x-organization-id",
1111
1160
  "x-internal-api-key"
@@ -1227,4 +1276,4 @@ const arc = {
1227
1276
  };
1228
1277
 
1229
1278
  //#endregion
1230
- export { ArcApiError, KNOWN_ARC_ERROR_CODES, _getAuthErrorHandler, _isAuthRecoverable, _resetArcFetchClient, _resetAuthRecovery, _resetAuthWarnings, _resolveRefreshedToken, _runAuthRecovery, arc, arcAuthHeaders, arcFetch, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createAuthRefreshHandler, createClient, createQueryString, createServerClient, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, getQuotaDetails, handleApiRequest, hasGlobalStaticAuth, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isQuotaExceeded, isValidationError };
1279
+ export { ArcApiError, KNOWN_ARC_ERROR_CODES, _getAuthErrorHandler, _isAuthRecoverable, _resetArcFetchClient, _resetAuthRecovery, _resetAuthWarnings, _resolveRefreshedToken, _runAuthRecovery, arc, arcAuthHeaders, arcFetch, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createAuthRefreshHandler, createClient, createQueryString, createServerClient, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, getQuotaDetails, getTierRequirement, handleApiRequest, hasGlobalStaticAuth, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isQuotaExceeded, isTierRequiredError, isValidationError };
@@ -1,5 +1,4 @@
1
1
  import { ClientEncryptionConfig } from "./client.js";
2
-
3
2
  //#region src/encryption.d.ts
4
3
  /** Key material accepted by jose in browser + Node (Web Crypto). */
5
4
  type JoseKey = CryptoKey | Uint8Array;
package/dist/hooks.d.ts CHANGED
@@ -10,7 +10,6 @@ import { TreeMethods } from "./presets/tree.js";
10
10
  import { DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, RequestPassthrough } from "./query.js";
11
11
  import { QueryKey, UseQueryResult } from "@tanstack/react-query";
12
12
  import { PaginatedResult } from "@classytic/repo-core/pagination";
13
-
14
13
  //#region src/hooks.d.ts
15
14
  /**
16
15
  * CRUD API interface accepted by createCrudHooks.
@@ -25,7 +24,8 @@ import { PaginatedResult } from "@classytic/repo-core/pagination";
25
24
  type CrudApi<T = unknown, TCreate = Partial<T>, TUpdate = Partial<T>> = Pick<BaseApi<T, TCreate, TUpdate>, "getAll" | "getById" | "create" | "update" | "delete" | "count"> & {
26
25
  upload?: BaseApi<T, TCreate, TUpdate>["upload"];
27
26
  dispatchAction?: BaseApi<T, TCreate, TUpdate>["dispatchAction"];
28
- invokeRoute?: BaseApi<T, TCreate, TUpdate>["invokeRoute"]; /** Declared aggregations (arc 2.13+). Always available on BaseApi. */
27
+ invokeRoute?: BaseApi<T, TCreate, TUpdate>["invokeRoute"];
28
+ /** Declared aggregations (arc 2.13+). Always available on BaseApi. */
29
29
  aggregate?: BaseApi<T, TCreate, TUpdate>["aggregate"];
30
30
  } & Partial<SoftDeleteMethods<T>> & Partial<BulkMethods<T, TCreate, TUpdate>> & Partial<SlugLookupMethods<T>> & Partial<TreeMethods<T>> & Partial<SearchPresetMethods<T>>;
31
31
  /** Args + options for `useAggregation`. Mirrors `ListQueryOptions` for DX consistency. */
@@ -182,11 +182,15 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
182
182
  KEYS: QueryKeys;
183
183
  cache: CacheUtils<T>;
184
184
  useList: {
185
- /** New signature — auto-injects token/orgId from configureAuth() context */(params?: Record<string, unknown>, options?: ListQueryOptions<T>): ListQueryResult<T>; /** Legacy signature — explicit token */
185
+ /** New signature — auto-injects token/orgId from configureAuth() context */
186
+ (params?: Record<string, unknown>, options?: ListQueryOptions<T>): ListQueryResult<T>;
187
+ /** Legacy signature — explicit token */
186
188
  (token: string | null, params?: Record<string, unknown>, options?: ListQueryOptions<T>): ListQueryResult<T>;
187
189
  };
188
190
  useDetail: {
189
- /** New signature — auto-injects token from configureAuth() context */(id: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>; /** Legacy signature — explicit token */
191
+ /** New signature — auto-injects token from configureAuth() context */
192
+ (id: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>;
193
+ /** Legacy signature — explicit token */
190
194
  (id: string | null, token: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>;
191
195
  };
192
196
  /**
@@ -202,7 +206,9 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
202
206
  */
203
207
  useSuspenseDetail: (id: string, options?: Omit<DetailQueryOptions<T>, "enabled">) => DetailQueryResult<T>;
204
208
  useInfiniteList: {
205
- /** New signature — auto-injects token/orgId from configureAuth() context */(params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>; /** Legacy signature — explicit token */
209
+ /** New signature — auto-injects token/orgId from configureAuth() context */
210
+ (params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>;
211
+ /** Legacy signature — explicit token */
206
212
  (token: string | null, params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>;
207
213
  };
208
214
  useActions: () => CrudActions<T, TCreate, TUpdate>;
@@ -223,7 +229,8 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
223
229
  * (approve/cancel/dispatch) instead of bespoke routes.
224
230
  */
225
231
  useAction: <TResult = T, TBody extends Record<string, unknown> = Record<string, unknown>>(options?: {
226
- invalidateQueries?: QueryKey[]; /** Default action name. Can be overridden per-call via `mutate({ action })`. */
232
+ invalidateQueries?: QueryKey[];
233
+ /** Default action name. Can be overridden per-call via `mutate({ action })`. */
227
234
  action?: string;
228
235
  messages?: MutationMessages<TResult, {
229
236
  id: string;
@@ -335,15 +342,20 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
335
342
  * `ssePlugin` (`/events/stream`). Pass `enabled: false` to opt out.
336
343
  */
337
344
  useResourceSync: (options?: {
338
- source?: "ws" | "sse"; /** Override resource name. Defaults to the factory's `entityKey`. */
339
- resource?: string; /** Override path (default: `/ws` or `/events/stream`). */
340
- path?: string; /** Whether the connection is active. Default: true. */
341
- enabled?: boolean; /** Per-event hook fired AFTER cache invalidation. */
345
+ source?: "ws" | "sse";
346
+ /** Override resource name. Defaults to the factory's `entityKey`. */
347
+ resource?: string;
348
+ /** Override path (default: `/ws` or `/events/stream`). */
349
+ path?: string;
350
+ /** Whether the connection is active. Default: true. */
351
+ enabled?: boolean;
352
+ /** Per-event hook fired AFTER cache invalidation. */
342
353
  onEvent?: (event: {
343
354
  operation: "created" | "updated" | "deleted";
344
355
  id?: string;
345
356
  data: unknown;
346
- }) => void; /** Connection-state listener. */
357
+ }) => void;
358
+ /** Connection-state listener. */
347
359
  onConnectionChange?: (connected: boolean) => void;
348
360
  }) => {
349
361
  isConnected: boolean;
@@ -360,15 +372,6 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
360
372
  * configureNavigation(useRouter);
361
373
  */
362
374
  declare function configureNavigation(hook: UseRouterHook): void;
363
- declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>({
364
- api,
365
- entityKey,
366
- singular,
367
- plural,
368
- idField,
369
- defaults,
370
- callbacks,
371
- client
372
- }: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
375
+ declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>({ api, entityKey, singular, plural, idField, defaults, callbacks, client }: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
373
376
  //#endregion
374
377
  export { AggregationQueryOptions, BulkActions, CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };
package/dist/hooks.js CHANGED
@@ -305,11 +305,12 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
305
305
  tempId = resolveItemId(data) ?? `temp-${globalThis.crypto?.randomUUID?.() ?? Date.now()}`;
306
306
  tempIdsRef.current.set(variables, tempId);
307
307
  }
308
- return prependToListCache(oldData, {
308
+ const optimisticItem = {
309
309
  ...data,
310
310
  _optimistic: true,
311
311
  [idField ?? (resolveItemId(data) ? "id" : "_id")]: tempId
312
- });
312
+ };
313
+ return prependToListCache(oldData, optimisticItem);
313
314
  },
314
315
  reconcile: (raw, variables) => {
315
316
  const serverDoc = extractItem(raw);
@@ -462,7 +463,8 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
462
463
  create: useCallback(async (params, options) => {
463
464
  silentRef.current = options?.silent ?? false;
464
465
  try {
465
- const entity = extractItem(await createMutation.mutateAsync(resolveActionAuth(params)));
466
+ const raw = await createMutation.mutateAsync(resolveActionAuth(params));
467
+ const entity = extractItem(raw);
466
468
  options?.onSuccess?.(entity);
467
469
  options?.onSettled?.(entity, null);
468
470
  return entity;
@@ -477,7 +479,8 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
477
479
  update: useCallback(async (params, options) => {
478
480
  silentRef.current = options?.silent ?? false;
479
481
  try {
480
- const entity = extractItem(await enqueueWrite(params.id, () => updateMutation.mutateAsync(resolveActionAuth(params))));
482
+ const raw = await enqueueWrite(params.id, () => updateMutation.mutateAsync(resolveActionAuth(params)));
483
+ const entity = extractItem(raw);
481
484
  options?.onSuccess?.(entity);
482
485
  options?.onSettled?.(entity, null);
483
486
  return entity;
@@ -507,7 +510,8 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
507
510
  restore: useCallback(async (params, options) => {
508
511
  silentRef.current = options?.silent ?? false;
509
512
  try {
510
- const entity = extractItem(await enqueueWrite(params.id, () => restoreMutation.mutateAsync(resolveActionAuth(params))));
513
+ const raw = await enqueueWrite(params.id, () => restoreMutation.mutateAsync(resolveActionAuth(params)));
514
+ const entity = extractItem(raw);
511
515
  options?.onSuccess?.(entity);
512
516
  options?.onSettled?.(entity, null);
513
517
  return entity;
@@ -1,7 +1,5 @@
1
1
  import { ToastHandler } from "./client.js";
2
- import * as _$_tanstack_react_query0 from "@tanstack/react-query";
3
2
  import { QueryClient, QueryKey, UseMutateAsyncFunction, UseMutateFunction } from "@tanstack/react-query";
4
-
5
3
  //#region src/mutation.d.ts
6
4
  /**
7
5
  * Toast copy for a mutation. Generic over the mutation's result/variables so
@@ -152,7 +150,7 @@ interface CreateOptimisticMutationConfig<TData, TVariables> {
152
150
  shouldToast?: () => boolean;
153
151
  toastHandler?: ToastHandler;
154
152
  }
155
- declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimisticMutationConfig<TData, TVariables>): _$_tanstack_react_query0.UseMutationResult<TData, Error, TVariables, {
153
+ declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimisticMutationConfig<TData, TVariables>): import("@tanstack/react-query").UseMutationResult<TData, Error, TVariables, {
156
154
  previous: {
157
155
  key: readonly unknown[];
158
156
  data: [readonly unknown[], unknown][];
package/dist/mutation.js CHANGED
@@ -82,8 +82,10 @@ function useMutationWithTransition(config) {
82
82
  queryClient.invalidateQueries({ queryKey: key });
83
83
  });
84
84
  };
85
- if (invalidateQueries.length > 0 && (config.shouldInvalidate?.(data) ?? true)) if (withTransition) startTransition(invalidate);
86
- else invalidate();
85
+ if (invalidateQueries.length > 0 && (config.shouldInvalidate?.(data) ?? true)) {
86
+ if (withTransition) startTransition(invalidate);
87
+ else invalidate();
88
+ }
87
89
  if (toast && (config.shouldToast?.() ?? true)) showToast("success", messages, data, variables, void 0, instanceToast);
88
90
  onSuccess?.(data, variables);
89
91
  },
@@ -1,6 +1,5 @@
1
1
  import { EntityReadApi } from "./query-options.js";
2
2
  import { HydrationBoundary, InfiniteData, QueryClient, dehydrate } from "@tanstack/react-query";
3
-
4
3
  //#region src/prefetch.d.ts
5
4
  interface PrefetchAuthContext {
6
5
  /** Auth token for protected endpoints. Required for bearer/header auth on server. */
@@ -1,6 +1,5 @@
1
1
  import { AnyBaseApi, CreateOf, DocOf, ScopedArgs, UpdateOf } from "../api.js";
2
2
  import { BulkCreateResult, DeleteManyResult, UpdateManyResult } from "@classytic/repo-core/repository";
3
-
4
3
  //#region src/presets/bulk.d.ts
5
4
  interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
6
5
  /** Insert many docs in one round-trip. Backend mounts `POST /:resource/bulk`. */
@@ -1,5 +1,4 @@
1
1
  import { AnyBaseApi, ScopedArgs } from "../api.js";
2
-
3
2
  //#region src/presets/history.d.ts
4
3
  /** One audit-trail entry — arc's `AuditEntry` wire shape for a single record. */
5
4
  interface HistoryEntry {
@@ -1,6 +1,5 @@
1
1
  import { AnyBaseApi, DocOf, ScopedArgs } from "../api.js";
2
2
  import { PaginatedResult } from "@classytic/repo-core/pagination";
3
-
4
3
  //#region src/presets/search.d.ts
5
4
  interface SearchPresetMethods<TDoc> {
6
5
  /**
@@ -8,8 +7,11 @@ interface SearchPresetMethods<TDoc> {
8
7
  * Backend mounts `POST /:resource/search` via `searchPreset()`.
9
8
  */
10
9
  searchEngine<TResult = TDoc, TBody extends Record<string, unknown> = Record<string, unknown>>(args?: ScopedArgs & {
11
- /** Free-text query forwarded as `body.query`. */query?: string; /** Engine-specific options merged into the request body. */
12
- body?: TBody; /** Override path (default `/search`). */
10
+ /** Free-text query forwarded as `body.query`. */
11
+ query?: string;
12
+ /** Engine-specific options merged into the request body. */
13
+ body?: TBody;
14
+ /** Override path (default `/search`). */
13
15
  path?: string;
14
16
  }): Promise<TResult[] | PaginatedResult<TResult>>;
15
17
  /**
@@ -17,9 +19,13 @@ interface SearchPresetMethods<TDoc> {
17
19
  * Backend mounts `POST /:resource/search-similar`.
18
20
  */
19
21
  searchSimilar<TResult = TDoc, TBody extends Record<string, unknown> = Record<string, unknown>>(args?: ScopedArgs & {
20
- /** Text query — backend embeds and searches for nearest neighbors. */query?: string; /** Pre-computed embedding vector — used directly for similarity search. */
21
- vector?: number[]; /** Vector-engine options (`topK`, `filter`, `index`, ...). */
22
- body?: TBody; /** Override path (default `/search-similar`). */
22
+ /** Text query — backend embeds and searches for nearest neighbors. */
23
+ query?: string;
24
+ /** Pre-computed embedding vector used directly for similarity search. */
25
+ vector?: number[];
26
+ /** Vector-engine options (`topK`, `filter`, `index`, ...). */
27
+ body?: TBody;
28
+ /** Override path (default `/search-similar`). */
23
29
  path?: string;
24
30
  }): Promise<TResult[]>;
25
31
  /**
@@ -27,8 +33,11 @@ interface SearchPresetMethods<TDoc> {
27
33
  * Backend mounts `POST /:resource/embed`.
28
34
  */
29
35
  embed(args: ScopedArgs & {
30
- /** Text or array of texts to embed. */input: string | string[]; /** Embed-engine options (`model`, `dimensions`, ...). */
31
- body?: Record<string, unknown>; /** Override path (default `/embed`). */
36
+ /** Text or array of texts to embed. */
37
+ input: string | string[];
38
+ /** Embed-engine options (`model`, `dimensions`, ...). */
39
+ body?: Record<string, unknown>;
40
+ /** Override path (default `/embed`). */
32
41
  path?: string;
33
42
  }): Promise<number[] | number[][]>;
34
43
  }
@@ -1,5 +1,4 @@
1
1
  import { AnyBaseApi, DocOf, ScopedArgs } from "../api.js";
2
-
3
2
  //#region src/presets/slug.d.ts
4
3
  interface SlugLookupMethods<TDoc> {
5
4
  /** Fetch a single doc by slug. Backend mounts `GET /:resource/slug/:slug`. */
@@ -1,6 +1,5 @@
1
1
  import { AnyBaseApi, DocOf, QueryParams, ScopedArgs } from "../api.js";
2
2
  import { PaginatedResult } from "@classytic/repo-core/pagination";
3
-
4
3
  //#region src/presets/soft-delete.d.ts
5
4
  interface SoftDeleteMethods<TDoc> {
6
5
  /** List soft-deleted docs. Backend mounts `GET /:resource/deleted`. */
@@ -1,6 +1,5 @@
1
1
  import { AnyBaseApi, DocOf, QueryParams, ScopedArgs } from "../api.js";
2
2
  import { PaginatedResult } from "@classytic/repo-core/pagination";
3
-
4
3
  //#region src/presets/tree.d.ts
5
4
  interface TreeMethods<TDoc> {
6
5
  /** Fetch the full hierarchy. Backend mounts `GET /:resource/tree`. */
@@ -1,5 +1,4 @@
1
1
  import { QueryClient } from "@tanstack/react-query";
2
-
3
2
  //#region src/query-client.d.ts
4
3
  interface QueryClientOverrides {
5
4
  staleTime?: number;
@@ -3,8 +3,8 @@ import { QueryClient, defaultShouldDehydrateQuery, isServer } from "@tanstack/re
3
3
 
4
4
  //#region src/query-client.ts
5
5
  const DEFAULTS = {
6
- staleTime: 300 * 1e3,
7
- gcTime: 1800 * 1e3,
6
+ staleTime: 3e5,
7
+ gcTime: 18e5,
8
8
  retry: 0,
9
9
  refetchOnWindowFocus: false
10
10
  };
@@ -1,6 +1,4 @@
1
1
  import { QueryKeys } from "./cache.js";
2
- import * as _$_tanstack_react_query0 from "@tanstack/react-query";
3
-
4
2
  //#region src/query-options.d.ts
5
3
  /** Per-call request context: auth + Next.js fetch caching passthrough. */
6
4
  interface QueryFnContext {
@@ -91,57 +89,65 @@ type EntityQueries = ReturnType<typeof createEntityQueries>;
91
89
  * await queryClient.ensureQueryData(productQueries.detail(id, { token }));
92
90
  */
93
91
  declare function createEntityQueries(api: EntityReadApi, entityKey: string): {
94
- /** The entity's key factory — for invalidation / setQueryData at call sites. */keys: QueryKeys; /** GET /:resource — mirrors `useList`'s key (scoped, org-normalized). */
95
- list(params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
96
- queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
92
+ /** The entity's key factory — for invalidation / setQueryData at call sites. */
93
+ keys: QueryKeys;
94
+ /** GET /:resource — mirrors `useList`'s key (scoped, org-normalized). */
95
+ list(params?: Record<string, unknown>, ctx?: QueryFnContext): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
96
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, readonly unknown[], never> | undefined;
97
97
  } & {
98
98
  queryKey: readonly unknown[] & {
99
99
  [dataTagSymbol]: unknown;
100
100
  [dataTagErrorSymbol]: Error;
101
101
  };
102
- }; /** GET /:resource/:id — mirrors `useDetail`'s scoped key (+ params variant). */
103
- detail(id: string, opts?: DetailQueryOpts): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
104
- queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
102
+ };
103
+ /** GET /:resource/:id mirrors `useDetail`'s scoped key (+ params variant). */
104
+ detail(id: string, opts?: DetailQueryOpts): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
105
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, readonly unknown[], never> | undefined;
105
106
  } & {
106
107
  queryKey: readonly unknown[] & {
107
108
  [dataTagSymbol]: unknown;
108
109
  [dataTagErrorSymbol]: Error;
109
110
  };
110
- }; /** GET /:resource/slug/:slug — mirrors `useDetailBySlug`'s key. */
111
- bySlug(slug: string, opts?: DetailQueryOpts): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
112
- queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
111
+ };
112
+ /** GET /:resource/slug/:slug mirrors `useDetailBySlug`'s key. */
113
+ bySlug(slug: string, opts?: DetailQueryOpts): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
114
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, readonly unknown[], never> | undefined;
113
115
  } & {
114
116
  queryKey: readonly unknown[] & {
115
117
  [dataTagSymbol]: unknown;
116
118
  [dataTagErrorSymbol]: Error;
117
119
  };
118
- }; /** GET /:resource/deleted — mirrors `useDeleted`'s key. */
119
- deleted(params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
120
- queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
120
+ };
121
+ /** GET /:resource/deleted mirrors `useDeleted`'s key. */
122
+ deleted(params?: Record<string, unknown>, ctx?: QueryFnContext): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
123
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, readonly unknown[], never> | undefined;
121
124
  } & {
122
125
  queryKey: readonly unknown[] & {
123
126
  [dataTagSymbol]: unknown;
124
127
  [dataTagErrorSymbol]: Error;
125
128
  };
126
- }; /** GET /:resource/tree — mirrors `useTree`'s key. */
127
- tree(params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
128
- queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
129
+ };
130
+ /** GET /:resource/tree mirrors `useTree`'s key. */
131
+ tree(params?: Record<string, unknown>, ctx?: QueryFnContext): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
132
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, readonly unknown[], never> | undefined;
129
133
  } & {
130
134
  queryKey: readonly unknown[] & {
131
135
  [dataTagSymbol]: unknown;
132
136
  [dataTagErrorSymbol]: Error;
133
137
  };
134
- }; /** GET /:resource/:parentId/children — mirrors `useChildren`'s key. */
135
- children(parentId: string, params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
136
- queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
138
+ };
139
+ /** GET /:resource/:parentId/children mirrors `useChildren`'s key. */
140
+ children(parentId: string, params?: Record<string, unknown>, ctx?: QueryFnContext): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
141
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, readonly unknown[], never> | undefined;
137
142
  } & {
138
143
  queryKey: readonly unknown[] & {
139
144
  [dataTagSymbol]: unknown;
140
145
  [dataTagErrorSymbol]: Error;
141
146
  };
142
- }; /** GET /:resource/aggregations/:name — mirrors `useAggregation`'s tenant-scoped key. */
143
- aggregation(name: string, filter?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
144
- queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, readonly unknown[], never> | undefined;
147
+ };
148
+ /** GET /:resource/aggregations/:name mirrors `useAggregation`'s tenant-scoped key. */
149
+ aggregation(name: string, filter?: Record<string, unknown>, ctx?: QueryFnContext): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<unknown, Error, unknown, readonly unknown[]>, "queryFn"> & {
150
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, readonly unknown[], never> | undefined;
145
151
  } & {
146
152
  queryKey: readonly unknown[] & {
147
153
  [dataTagSymbol]: unknown;
@@ -152,11 +158,11 @@ declare function createEntityQueries(api: EntityReadApi, entityKey: string): {
152
158
  * Infinite list — mirrors `useInfiniteList`'s key (`scopedList + 'infinite'`)
153
159
  * and its page-param semantics (keyset cursor or offset page + 1).
154
160
  */
155
- infiniteList(params?: Record<string, unknown>, ctx?: QueryFnContext): _$_tanstack_react_query0.OmitKeyof<_$_tanstack_react_query0.UseInfiniteQueryOptions<unknown, Error, _$_tanstack_react_query0.InfiniteData<unknown, unknown>, unknown[], unknown>, "queryFn"> & {
156
- queryFn?: _$_tanstack_react_query0.QueryFunction<unknown, unknown[], unknown> | undefined;
161
+ infiniteList(params?: Record<string, unknown>, ctx?: QueryFnContext): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseInfiniteQueryOptions<unknown, Error, import("@tanstack/react-query").InfiniteData<unknown, unknown>, unknown[], unknown>, "queryFn"> & {
162
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, unknown[], unknown> | undefined;
157
163
  } & {
158
164
  queryKey: unknown[] & {
159
- [dataTagSymbol]: _$_tanstack_react_query0.InfiniteData<unknown, unknown>;
165
+ [dataTagSymbol]: import("@tanstack/react-query").InfiniteData<unknown, unknown>;
160
166
  [dataTagErrorSymbol]: Error;
161
167
  };
162
168
  };
package/dist/query.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { CacheUtils, DEFAULT_QUERY_CONFIG, PaginationData, QUERY_CONFIGS, QueryKeys, createCacheUtils, createQueryKeys, extractItem, extractItems, getItemId, normalizePagination, updateListCache } from "./cache.js";
2
2
  import { InfiniteData, QueryClient, QueryKey } from "@tanstack/react-query";
3
-
4
3
  //#region src/query.d.ts
5
4
  /** Request-level options passed through to the fetch call */
6
5
  interface RequestPassthrough {
@@ -143,13 +142,7 @@ interface CreateListQueryConfig {
143
142
  * directly via `placeholderData`, so the detail GET still fires while the
144
143
  * consumer sees an instant list-shaped preview. See `findItemInListCache`.
145
144
  */
146
- declare function useListQuery<T>({
147
- queryKey,
148
- queryFn,
149
- enabled,
150
- options,
151
- select
152
- }: CreateListQueryConfig): ListQueryResult<T>;
145
+ declare function useListQuery<T>({ queryKey, queryFn, enabled, options, select }: CreateListQueryConfig): ListQueryResult<T>;
153
146
  /**
154
147
  * Find an item in any list cache for this entity by ID.
155
148
  *
@@ -187,28 +180,11 @@ interface CreateDetailQueryConfig<T = unknown> {
187
180
  */
188
181
  placeholderData?: () => T | undefined;
189
182
  }
190
- declare function useDetailQuery<T>({
191
- queryKey,
192
- queryFn,
193
- enabled,
194
- options,
195
- select,
196
- placeholderData
197
- }: CreateDetailQueryConfig<T>): DetailQueryResult<T>;
183
+ declare function useDetailQuery<T>({ queryKey, queryFn, enabled, options, select, placeholderData }: CreateDetailQueryConfig<T>): DetailQueryResult<T>;
198
184
  /** Suspense variant of {@link useListQuery}. Always enabled; suspends until resolved. */
199
- declare function useSuspenseListQuery<T>({
200
- queryKey,
201
- queryFn,
202
- options,
203
- select
204
- }: Omit<CreateListQueryConfig, "enabled">): ListQueryResult<T>;
185
+ declare function useSuspenseListQuery<T>({ queryKey, queryFn, options, select }: Omit<CreateListQueryConfig, "enabled">): ListQueryResult<T>;
205
186
  /** Suspense variant of {@link useDetailQuery}. Always enabled; suspends until resolved. */
206
- declare function useSuspenseDetailQuery<T>({
207
- queryKey,
208
- queryFn,
209
- options,
210
- select
211
- }: Omit<CreateDetailQueryConfig<T>, "enabled" | "placeholderData">): DetailQueryResult<T>;
187
+ declare function useSuspenseDetailQuery<T>({ queryKey, queryFn, options, select }: Omit<CreateDetailQueryConfig<T>, "enabled" | "placeholderData">): DetailQueryResult<T>;
212
188
  interface InfiniteListQueryOptions {
213
189
  public?: boolean;
214
190
  enabled?: boolean;
@@ -257,16 +233,7 @@ interface CreateInfiniteListQueryConfig {
257
233
  /** Max pages to keep in memory. Old pages are evicted when exceeded. */
258
234
  maxPages?: number;
259
235
  }
260
- declare function useInfiniteListQuery<T>({
261
- queryKey,
262
- queryFn,
263
- enabled,
264
- options,
265
- initialPageParam,
266
- getNextPageParam,
267
- getPreviousPageParam,
268
- maxPages
269
- }: CreateInfiniteListQueryConfig): InfiniteListQueryResult<T>;
236
+ declare function useInfiniteListQuery<T>({ queryKey, queryFn, enabled, options, initialPageParam, getNextPageParam, getPreviousPageParam, maxPages }: CreateInfiniteListQueryConfig): InfiniteListQueryResult<T>;
270
237
  /** Recognized data-freshness presets. Maps to QUERY_CONFIGS. */
271
238
  type QueryFreshness = keyof typeof QUERY_CONFIGS;
272
239
  /**
@@ -342,13 +309,6 @@ interface UseApiQueryResult<TData> {
342
309
  * select: (res) => res.entries,
343
310
  * });
344
311
  */
345
- declare function useApiQuery<TResponse = unknown, TData = ExtractData<TResponse>>({
346
- queryKey,
347
- queryFn,
348
- enabled,
349
- freshness,
350
- select,
351
- options
352
- }: UseApiQueryConfig<TResponse, TData>): UseApiQueryResult<TData>;
312
+ declare function useApiQuery<TResponse = unknown, TData = ExtractData<TResponse>>({ queryKey, queryFn, enabled, freshness, select, options }: UseApiQueryConfig<TResponse, TData>): UseApiQueryResult<TData>;
353
313
  //#endregion
354
314
  export { type CacheUtils, CreateDetailQueryConfig, CreateInfiniteListQueryConfig, CreateListQueryConfig, DEFAULT_QUERY_CONFIG, DetailQueryOptions, DetailQueryResult, ExtractData, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, type PaginationData, QUERY_CONFIGS, QueryFreshness, type QueryKeys, RequestPassthrough, UseApiQueryConfig, UseApiQueryOptions, UseApiQueryResult, createCacheUtils, createQueryKeys, extractItem, extractItems, findItemInListCache, getItemId, normalizePagination, updateListCache, useApiQuery, useDetailQuery, useInfiniteListQuery, useListQuery, useSuspenseDetailQuery, useSuspenseListQuery };
package/dist/sse.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { QueryKey } from "@tanstack/react-query";
2
-
3
2
  //#region src/sse.d.ts
4
3
  /**
5
4
  * Build an authenticated SSE URL using the global client + auth singletons.
package/dist/sse.js CHANGED
@@ -113,13 +113,14 @@ function subscribeToEvents(options) {
113
113
  } catch {
114
114
  payload = event.data;
115
115
  }
116
- dispatch(typeof payload === "object" && payload !== null && "type" in payload && "data" in payload ? payload : {
116
+ const parsed = typeof payload === "object" && payload !== null && "type" in payload && "data" in payload ? payload : {
117
117
  type: eventType,
118
118
  resource: resource ?? eventType.split(".")[0] ?? "",
119
119
  data: payload,
120
120
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
121
121
  ...event.lastEventId ? { id: event.lastEventId } : {}
122
- });
122
+ };
123
+ dispatch(parsed);
123
124
  });
124
125
  es.onerror = () => {
125
126
  try {
package/dist/upload.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { ArcClient, HttpMethod, ToastHandler } from "./client.js";
2
2
  import { MutationMessages } from "./mutation.js";
3
3
  import { QueryKey } from "@tanstack/react-query";
4
-
5
4
  //#region src/upload.d.ts
6
5
  /**
7
6
  * Upload-progress snapshot. Emitted on every native `xhr.upload.progress`
package/dist/upload.js CHANGED
@@ -150,8 +150,9 @@ function uploadAttempt(options) {
150
150
  const lengthComputable = event.lengthComputable;
151
151
  const total = lengthComputable ? event.total : 0;
152
152
  const loaded = event.loaded;
153
+ const percent = lengthComputable && total > 0 ? Math.min(100, Math.round(loaded / total * 100)) : 0;
153
154
  onProgress({
154
- percent: lengthComputable && total > 0 ? Math.min(100, Math.round(loaded / total * 100)) : 0,
155
+ percent,
155
156
  loaded,
156
157
  total,
157
158
  lengthComputable
@@ -180,7 +181,8 @@ function uploadAttempt(options) {
180
181
  resolve(body);
181
182
  return;
182
183
  }
183
- reject(new ArcApiError(extractErrorMessage(body, statusText), {
184
+ const message = extractErrorMessage(body, statusText);
185
+ reject(new ArcApiError(message, {
184
186
  status,
185
187
  statusText,
186
188
  json: body,
package/dist/ws.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { QueryKey } from "@tanstack/react-query";
2
-
3
2
  //#region src/ws.d.ts
4
3
  /**
5
4
  * Inbound message shape from Arc's `websocketPlugin` broadcasts.
package/dist/ws.js CHANGED
@@ -123,18 +123,19 @@ function connectWs(options = {}) {
123
123
  const isAuthClose = event.code === 1008 || event.code === 3401 || event.code === 4001 || event.code === 4401;
124
124
  if (handler && isAuthClose && wsAuthRetries < maxAuthRetries) {
125
125
  wsAuthRetries += 1;
126
+ const synthError = new ArcApiError(event.reason || `WebSocket closed with auth code ${event.code}`, {
127
+ status: 401,
128
+ statusText: "WebSocket auth failure",
129
+ json: {
130
+ code: "arc.websocket.unauthorized",
131
+ wsCloseCode: event.code,
132
+ reason: event.reason
133
+ },
134
+ endpoint: url ?? path,
135
+ method: "GET"
136
+ });
126
137
  _runAuthRecovery(handler, {
127
- error: new ArcApiError(event.reason || `WebSocket closed with auth code ${event.code}`, {
128
- status: 401,
129
- statusText: "WebSocket auth failure",
130
- json: {
131
- code: "arc.websocket.unauthorized",
132
- wsCloseCode: event.code,
133
- reason: event.reason
134
- },
135
- endpoint: url ?? path,
136
- method: "GET"
137
- }),
138
+ error: synthError,
138
139
  request: {
139
140
  method: "GET",
140
141
  endpoint: url ?? path
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc-next",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "React + TanStack Query SDK for Arc resources",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -148,7 +148,7 @@
148
148
  "check:dead-code": "knip"
149
149
  },
150
150
  "peerDependencies": {
151
- "@classytic/repo-core": ">=0.14.0",
151
+ "@classytic/repo-core": ">=0.24.0",
152
152
  "@tanstack/react-query": ">=5.62.0",
153
153
  "jose": ">=5.0.0",
154
154
  "react": ">=19.0.0"
@@ -171,7 +171,7 @@
171
171
  "@arethetypeswrong/cli": "^0.18.5",
172
172
  "@biomejs/biome": "^2.5.5",
173
173
  "@classytic/dev-tools": "^0.2.0",
174
- "@classytic/repo-core": "^0.14.0",
174
+ "@classytic/repo-core": ">=0.24.0",
175
175
  "@tanstack/react-query": "^5.97.0",
176
176
  "@testing-library/react": "^16.3.2",
177
177
  "@types/react": "^19.2.14",
@@ -182,7 +182,7 @@
182
182
  "publint": "^0.3.21",
183
183
  "react": "^19.2.5",
184
184
  "react-dom": "^19.2.5",
185
- "tsdown": "^0.21.7",
185
+ "tsdown": "^0.22.14",
186
186
  "typescript": "^6.0.2",
187
187
  "vitest": "^4.1.4"
188
188
  }