@classytic/arc-next 0.7.1 → 0.8.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
@@ -171,6 +171,39 @@ declare function isValidationError(error: unknown): error is ArcApiError;
171
171
  * Prisma P2002 → `arc.conflict` (with `details[].code === 'duplicate_key'`).
172
172
  */
173
173
  declare function isDuplicateKeyError(error: unknown): error is ArcApiError;
174
+ /**
175
+ * Application-Layer Encryption (ALE) config — the client half of
176
+ * `@classytic/arc/encryption`. Lets the SDK decrypt JWE response bodies and
177
+ * (optionally) encrypt JSON request bodies, transparently to callers and to
178
+ * the repo-core wire contracts (decryption happens BEFORE error / pagination
179
+ * parsing, so the parsed shape is identical to an unencrypted response).
180
+ *
181
+ * `decrypt` / `encrypt` are host-supplied so the core SDK pulls no crypto
182
+ * dependency. Use `createJoseEncryption()` from `@classytic/arc-next/encryption`
183
+ * for a ready-made JWE implementation, or wire your own (KMS proxy, WebCrypto).
184
+ */
185
+ interface ClientEncryptionConfig {
186
+ /**
187
+ * Decrypt an encrypted response body (JWE compact string) → plaintext JSON
188
+ * string. Required — this is what makes encrypted responses readable.
189
+ */
190
+ decrypt: (payload: string) => string | Promise<string>;
191
+ /**
192
+ * Encrypt a serialized JSON request body string → JWE compact string.
193
+ * Required only when `encryptRequests` is true.
194
+ */
195
+ encrypt?: (json: string) => string | Promise<string>;
196
+ /**
197
+ * Encrypt outbound JSON request bodies. Default `false` — responses are
198
+ * still decrypted regardless (the common case: server encrypts responses,
199
+ * client posts plaintext over TLS).
200
+ */
201
+ encryptRequests?: boolean;
202
+ /** Response media type signalling an encrypted body. Default `'application/jose'`. */
203
+ responseContentType?: string;
204
+ /** Content-Type set on encrypted outbound requests. Default `'application/jose'`. */
205
+ requestContentType?: string;
206
+ }
174
207
  interface ClientConfig {
175
208
  baseUrl: string;
176
209
  internalApiKey?: string;
@@ -253,6 +286,13 @@ interface ClientConfig {
253
286
  * });
254
287
  */
255
288
  afterResponse?: AfterResponseInterceptor;
289
+ /**
290
+ * Application-Layer Encryption. When set, the SDK decrypts `application/jose`
291
+ * response bodies (and, with `encryptRequests`, encrypts JSON request bodies)
292
+ * — the client counterpart to `@classytic/arc/encryption` on the backend.
293
+ * Transparent to repo-core wire contracts. See {@link ClientEncryptionConfig}.
294
+ */
295
+ encryption?: ClientEncryptionConfig;
256
296
  }
257
297
  interface RetryConfig {
258
298
  /** Total attempts including the first. `attempts: 3` means 1 try + 2 retries. Default: 0 (off). */
@@ -795,4 +835,4 @@ declare const arc: {
795
835
  delete: <T = unknown>(path: string, opts?: ArcFetchOptions) => Promise<T>;
796
836
  };
797
837
  //#endregion
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 };
838
+ export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcErrorCode, ArcFetchOptions, AuthConfig, AuthErrorContext, AuthErrorHandler, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, ClientEncryptionConfig, 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 };
package/dist/client.js CHANGED
@@ -763,6 +763,11 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
763
763
  const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
764
764
  let serializedBody = void 0;
765
765
  if (body !== void 0 && body !== null) serializedBody = isNonJsonBody(body) ? body : JSON.stringify(body);
766
+ const encryption = config.encryption;
767
+ if (encryption?.encryptRequests && encryption.encrypt && typeof serializedBody === "string") {
768
+ serializedBody = await encryption.encrypt(serializedBody);
769
+ headers["Content-Type"] = encryption.requestContentType ?? "application/jose";
770
+ }
766
771
  if (config.beforeRequest) {
767
772
  const ctx = await config.beforeRequest({
768
773
  method,
@@ -799,7 +804,7 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
799
804
  try {
800
805
  json = await response.clone().json();
801
806
  const j = json;
802
- errorMessage = typeof j?.error === "string" && j.error || typeof j?.message === "string" && j.message || response.statusText;
807
+ errorMessage = typeof j?.error === "string" && j.error || typeof j?.meta?.message === "string" && j.meta.message || typeof j?.message === "string" && j.message || response.statusText;
803
808
  } catch {
804
809
  try {
805
810
  const text = await response.text();
@@ -819,7 +824,12 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
819
824
  }
820
825
  const contentType = response.headers.get("Content-Type");
821
826
  let data;
822
- if (contentType?.includes("application/json")) data = await response.json();
827
+ const decryptCt = encryption ? encryption.responseContentType ?? "application/jose" : void 0;
828
+ if (encryption && decryptCt && contentType?.includes(decryptCt)) {
829
+ const cipher = await response.text();
830
+ const plaintext = cipher.length > 0 ? await encryption.decrypt(cipher) : "";
831
+ data = plaintext.length > 0 ? JSON.parse(plaintext) : void 0;
832
+ } else if (contentType?.includes("application/json")) data = await response.json();
823
833
  else if (contentType?.includes("application/pdf") || contentType?.includes("image/")) data = {
824
834
  data: await response.blob(),
825
835
  response
@@ -0,0 +1,36 @@
1
+ import { ClientEncryptionConfig } from "./client.js";
2
+
3
+ //#region src/encryption.d.ts
4
+ /** Key material accepted by jose in browser + Node (Web Crypto). */
5
+ type JoseKey = CryptoKey | Uint8Array;
6
+ interface JoseEncryptionOptions {
7
+ /** Single decryption key (private/secret) used for every response. */
8
+ decryptionKey?: JoseKey;
9
+ /**
10
+ * Decryption keys indexed by `kid` (rotation). When set, the inbound JWE's
11
+ * `kid` header selects the key — keep 2–3 entries across a rotation so
12
+ * in-flight responses signed with the previous `kid` still decrypt.
13
+ */
14
+ decryptionKeys?: Record<string, JoseKey>;
15
+ /** Public/secret key + `kid` used to encrypt outbound requests. */
16
+ encryptionKey?: {
17
+ kid: string;
18
+ key: JoseKey;
19
+ alg?: string;
20
+ };
21
+ /** Key-management algorithm. Default `'RSA-OAEP-256'`. */
22
+ alg?: string;
23
+ /** Content-encryption algorithm. Default `'A256GCM'`. */
24
+ enc?: string;
25
+ /** Inbound-decryption allowlists (anti-downgrade). Default `[alg]` / `[enc]`. */
26
+ allowedAlgs?: string[];
27
+ allowedEncs?: string[];
28
+ }
29
+ /**
30
+ * Build the `encrypt` / `decrypt` half of a {@link ClientEncryptionConfig}
31
+ * using `jose`. Spread the result into `encryption` and add `encryptRequests`
32
+ * / content-type overrides as needed.
33
+ */
34
+ declare function createJoseEncryption(options: JoseEncryptionOptions): Pick<ClientEncryptionConfig, 'encrypt' | 'decrypt'>;
35
+ //#endregion
36
+ export { JoseEncryptionOptions, createJoseEncryption };
@@ -0,0 +1,75 @@
1
+ import { CompactEncrypt, compactDecrypt } from "jose";
2
+
3
+ //#region src/encryption.ts
4
+ /**
5
+ * `@classytic/arc-next/encryption` — JWE helper for the client SDK.
6
+ *
7
+ * A ready-made `ClientEncryptionConfig` backed by `jose` (panva) — the same
8
+ * library `@classytic/arc/encryption` uses server-side. Works in the browser
9
+ * and Node (jose runs on Web Crypto). `jose` is an OPTIONAL peer: import this
10
+ * subpath only when you want JWE; the core SDK pulls no crypto dependency.
11
+ *
12
+ * Asymmetric key model (mirrors Visa MLE / the arc backend plugin):
13
+ * - Decrypt RESPONSES with the client's PRIVATE key (the recipient).
14
+ * - Encrypt REQUESTS with the SERVER's PUBLIC key, tagged by `kid`.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { configureClient } from "@classytic/arc-next/client";
19
+ * import { createJoseEncryption } from "@classytic/arc-next/encryption";
20
+ * import { importPKCS8, importSPKI } from "jose";
21
+ *
22
+ * configureClient({
23
+ * baseUrl: "https://api.example.com",
24
+ * encryption: {
25
+ * ...createJoseEncryption({
26
+ * decryptionKeys: { "client-1": await importPKCS8(clientPrivPem, "RSA-OAEP-256") },
27
+ * encryptionKey: { kid: "server-1", key: await importSPKI(serverPubPem, "RSA-OAEP-256") },
28
+ * }),
29
+ * encryptRequests: true,
30
+ * },
31
+ * });
32
+ * ```
33
+ */
34
+ /**
35
+ * Build the `encrypt` / `decrypt` half of a {@link ClientEncryptionConfig}
36
+ * using `jose`. Spread the result into `encryption` and add `encryptRequests`
37
+ * / content-type overrides as needed.
38
+ */
39
+ function createJoseEncryption(options) {
40
+ const alg = options.alg ?? "RSA-OAEP-256";
41
+ const enc = options.enc ?? "A256GCM";
42
+ const allowedAlgs = options.allowedAlgs ?? [alg];
43
+ const allowedEncs = options.allowedEncs ?? [enc];
44
+ const decoder = new TextDecoder();
45
+ const encoder = new TextEncoder();
46
+ const decrypt = async (payload) => {
47
+ const { plaintext } = await compactDecrypt(payload, async (header) => {
48
+ if (options.decryptionKeys) {
49
+ const key = header.kid ? options.decryptionKeys[header.kid] : void 0;
50
+ if (!key) throw new Error(`[arc-next/encryption] no decryption key for kid '${header.kid ?? "<none>"}'`);
51
+ return key;
52
+ }
53
+ if (!options.decryptionKey) throw new Error("[arc-next/encryption] no decryption key configured");
54
+ return options.decryptionKey;
55
+ }, {
56
+ keyManagementAlgorithms: allowedAlgs,
57
+ contentEncryptionAlgorithms: allowedEncs
58
+ });
59
+ return decoder.decode(plaintext);
60
+ };
61
+ const encryptionKey = options.encryptionKey;
62
+ if (!encryptionKey) return { decrypt };
63
+ const encrypt = async (json) => new CompactEncrypt(encoder.encode(json)).setProtectedHeader({
64
+ alg: encryptionKey.alg ?? alg,
65
+ enc,
66
+ kid: encryptionKey.kid
67
+ }).encrypt(encryptionKey.key);
68
+ return {
69
+ encrypt,
70
+ decrypt
71
+ };
72
+ }
73
+
74
+ //#endregion
75
+ export { createJoseEncryption };
package/dist/hooks.d.ts CHANGED
@@ -180,6 +180,18 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
180
180
  /** New signature — auto-injects token from configureAuth() context */(id: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>; /** Legacy signature — explicit token */
181
181
  (id: string | null, token: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>;
182
182
  };
183
+ /**
184
+ * Suspense variant of `useList` for Suspense / streaming-SSR routes. Always
185
+ * fetches and SUSPENDS until resolved — wrap the subtree in `<Suspense>` and
186
+ * an error boundary. No `enabled` gate and no `keepPreviousData` preview
187
+ * (TanStack forbids both on suspense queries); `isLoading` is always `false`.
188
+ */
189
+ useSuspenseList: (params?: Record<string, unknown>, options?: Omit<ListQueryOptions<T>, "enabled">) => ListQueryResult<T>;
190
+ /**
191
+ * Suspense variant of `useDetail`. `id` must be present (it always fetches).
192
+ * Suspends until resolved; no list→detail `placeholderData` preview.
193
+ */
194
+ useSuspenseDetail: (id: string, options?: Omit<DetailQueryOptions<T>, "enabled">) => DetailQueryResult<T>;
183
195
  useInfiniteList: {
184
196
  /** New signature — auto-injects token/orgId from configureAuth() context */(params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>; /** Legacy signature — explicit token */
185
197
  (token: string | null, params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>;
package/dist/hooks.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { getAuthMode, getClientAuthContext, hasGlobalStaticAuth } from "./client.js";
4
4
  import { isKeysetPagination, isOffsetPagination } from "./api.js";
5
5
  import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, syncDetailToLists, updateListCache } from "./cache.js";
6
- import { findItemInListCache, useDetailQuery, useInfiniteListQuery, useListQuery } from "./query.js";
6
+ import { findItemInListCache, useDetailQuery, useInfiniteListQuery, useListQuery, useSuspenseDetailQuery, useSuspenseListQuery } from "./query.js";
7
7
  import { useMutationWithTransition, useOptimisticMutation } from "./mutation.js";
8
8
  import { subscribeToEvents } from "./sse.js";
9
9
  import { connectWs } from "./ws.js";
@@ -176,6 +176,82 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
176
176
  ]);
177
177
  return detailResult;
178
178
  }
179
+ function useSuspenseList(params = {}, options = {}) {
180
+ const auth = resolveAuth();
181
+ const token = auth.token;
182
+ let resolvedParams = params;
183
+ if (auth.organizationId && !resolvedParams.organizationId) resolvedParams = {
184
+ ...resolvedParams,
185
+ organizationId: auth.organizationId
186
+ };
187
+ const { organizationId, ...restParams } = resolvedParams;
188
+ const scope = options._scope || (organizationId ? "tenant" : "super-admin");
189
+ const { request: requestOpts, ...queryOpts } = options;
190
+ return useSuspenseListQuery({
191
+ queryKey: KEYS.scopedList(scope, {
192
+ organizationId,
193
+ ...restParams
194
+ }),
195
+ queryFn: ({ signal }) => api.getAll({
196
+ token,
197
+ organizationId,
198
+ params: restParams,
199
+ options: {
200
+ signal,
201
+ ...requestOpts
202
+ }
203
+ }),
204
+ options: {
205
+ staleTime: queryOpts.staleTime ?? config.staleTime,
206
+ gcTime: queryOpts.gcTime ?? config.gcTime,
207
+ refetchOnWindowFocus: queryOpts.refetchOnWindowFocus ?? config.refetchOnWindowFocus,
208
+ structuralSharing: queryOpts.structuralSharing ?? config.structuralSharing,
209
+ refetchInterval: queryOpts.refetchInterval,
210
+ refetchIntervalInBackground: queryOpts.refetchIntervalInBackground
211
+ },
212
+ select: queryOpts.select
213
+ });
214
+ }
215
+ function useSuspenseDetail(id, options = {}) {
216
+ const auth = resolveAuth();
217
+ const token = auth.token;
218
+ let opts = options;
219
+ if (auth.organizationId && !opts.organizationId) opts = {
220
+ ...opts,
221
+ organizationId: auth.organizationId
222
+ };
223
+ const { organizationId, params: queryParams, request: requestOpts, ...restOptions } = opts;
224
+ const detailKey = KEYS.scopedDetail(id, organizationId ?? null);
225
+ const fullDetailKey = queryParams ? [...detailKey, queryParams] : detailKey;
226
+ const queryClient = useQueryClient();
227
+ const detailResult = useSuspenseDetailQuery({
228
+ queryKey: fullDetailKey,
229
+ queryFn: ({ signal }) => api.getById({
230
+ id,
231
+ token,
232
+ organizationId,
233
+ params: queryParams,
234
+ options: {
235
+ signal,
236
+ ...requestOpts
237
+ }
238
+ }),
239
+ options: {
240
+ staleTime: restOptions.staleTime ?? config.staleTime,
241
+ gcTime: restOptions.gcTime ?? config.gcTime,
242
+ refetchOnWindowFocus: restOptions.refetchOnWindowFocus ?? config.refetchOnWindowFocus,
243
+ structuralSharing: restOptions.structuralSharing ?? config.structuralSharing,
244
+ refetchInterval: restOptions.refetchInterval,
245
+ refetchIntervalInBackground: restOptions.refetchIntervalInBackground
246
+ },
247
+ select: restOptions.select
248
+ });
249
+ useEffect(() => {
250
+ if (!detailResult.item) return;
251
+ syncDetailToLists(queryClient, KEYS.lists(), detailResult.item, idField ? { idField } : {});
252
+ }, [detailResult.item, queryClient]);
253
+ return detailResult;
254
+ }
179
255
  function useActions() {
180
256
  const queryClient = useQueryClient();
181
257
  const silentRef = useRef(false);
@@ -956,6 +1032,8 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
956
1032
  cache,
957
1033
  useList,
958
1034
  useDetail,
1035
+ useSuspenseList,
1036
+ useSuspenseDetail,
959
1037
  useInfiniteList,
960
1038
  useActions,
961
1039
  useBulkActions,
package/dist/query.d.ts CHANGED
@@ -195,6 +195,20 @@ declare function useDetailQuery<T>({
195
195
  select,
196
196
  placeholderData
197
197
  }: CreateDetailQueryConfig<T>): DetailQueryResult<T>;
198
+ /** 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>;
205
+ /** 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>;
198
212
  interface InfiniteListQueryOptions {
199
213
  public?: boolean;
200
214
  enabled?: boolean;
@@ -337,4 +351,4 @@ declare function useApiQuery<TResponse = unknown, TData = ExtractData<TResponse>
337
351
  options
338
352
  }: UseApiQueryConfig<TResponse, TData>): UseApiQueryResult<TData>;
339
353
  //#endregion
340
- 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 };
354
+ 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/query.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, extractItems, getItemId, normalizePagination, updateListCache } from "./cache.js";
4
- import { keepPreviousData, useInfiniteQuery, useQuery } from "@tanstack/react-query";
4
+ import { keepPreviousData, useInfiniteQuery, useQuery, useSuspenseQuery } from "@tanstack/react-query";
5
5
  import { useCallback, useMemo, useRef } from "react";
6
6
 
7
7
  //#region src/query.ts
@@ -99,6 +99,48 @@ function useDetailQuery({ queryKey, queryFn, enabled = true, options = {}, selec
99
99
  refetch: query.refetch
100
100
  };
101
101
  }
102
+ /** Suspense variant of {@link useListQuery}. Always enabled; suspends until resolved. */
103
+ function useSuspenseListQuery({ queryKey, queryFn, options = {}, select }) {
104
+ const query = useSuspenseQuery({
105
+ queryKey,
106
+ queryFn: ({ signal }) => queryFn({ signal }),
107
+ ...DEFAULT_QUERY_CONFIG,
108
+ ...options,
109
+ ...select ? { select } : {}
110
+ });
111
+ return {
112
+ items: useMemo(() => extractItems(query.data), [query.data]),
113
+ pagination: useMemo(() => normalizePagination(query.data), [query.data]),
114
+ isLoading: false,
115
+ isFetching: query.isFetching,
116
+ isError: query.isError,
117
+ isSuccess: query.isSuccess,
118
+ isStale: query.isStale,
119
+ error: query.error,
120
+ refetch: query.refetch
121
+ };
122
+ }
123
+ /** Suspense variant of {@link useDetailQuery}. Always enabled; suspends until resolved. */
124
+ function useSuspenseDetailQuery({ queryKey, queryFn, options = {}, select }) {
125
+ const query = useSuspenseQuery({
126
+ queryKey,
127
+ queryFn: ({ signal }) => queryFn({ signal }),
128
+ ...DEFAULT_QUERY_CONFIG,
129
+ ...options,
130
+ ...select ? { select } : {}
131
+ });
132
+ return {
133
+ item: extractItem(query.data),
134
+ isLoading: false,
135
+ isFetching: query.isFetching,
136
+ isError: query.isError,
137
+ isSuccess: query.isSuccess,
138
+ isStale: query.isStale,
139
+ isPlaceholderData: false,
140
+ error: query.error,
141
+ refetch: query.refetch
142
+ };
143
+ }
102
144
  function useInfiniteListQuery({ queryKey, queryFn, enabled = true, options = {}, initialPageParam = 1, getNextPageParam, getPreviousPageParam, maxPages }) {
103
145
  const query = useInfiniteQuery({
104
146
  queryKey,
@@ -189,4 +231,4 @@ function useApiQuery({ queryKey, queryFn, enabled = true, freshness, select, opt
189
231
  }
190
232
 
191
233
  //#endregion
192
- export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, extractItems, findItemInListCache, getItemId, normalizePagination, updateListCache, useApiQuery, useDetailQuery, useInfiniteListQuery, useListQuery };
234
+ export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, extractItems, findItemInListCache, getItemId, normalizePagination, updateListCache, useApiQuery, useDetailQuery, useInfiniteListQuery, useListQuery, useSuspenseDetailQuery, useSuspenseListQuery };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc-next",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "React + TanStack Query SDK for Arc resources",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -38,6 +38,10 @@
38
38
  "types": "./dist/client.d.ts",
39
39
  "default": "./dist/client.js"
40
40
  },
41
+ "./encryption": {
42
+ "types": "./dist/encryption.d.ts",
43
+ "default": "./dist/encryption.js"
44
+ },
41
45
  "./api": {
42
46
  "types": "./dist/api.d.ts",
43
47
  "default": "./dist/api.js"
@@ -124,12 +128,14 @@
124
128
  "peerDependencies": {
125
129
  "@classytic/repo-core": ">=0.4.0",
126
130
  "@tanstack/react-query": ">=5.62.0",
131
+ "jose": ">=5.0.0",
127
132
  "react": ">=19.0.0"
128
133
  },
129
134
  "peerDependenciesMeta": {
130
135
  "react": { "optional": false },
131
136
  "@tanstack/react-query": { "optional": false },
132
- "@classytic/repo-core": { "optional": false }
137
+ "@classytic/repo-core": { "optional": false },
138
+ "jose": { "optional": true }
133
139
  },
134
140
  "devDependencies": {
135
141
  "@classytic/dev-tools": "^0.2.0",
@@ -139,6 +145,7 @@
139
145
  "@testing-library/react": "^16.3.2",
140
146
  "@types/react": "^19.2.14",
141
147
  "@types/react-dom": "^19.2.3",
148
+ "jose": "^6.2.3",
142
149
  "jsdom": "^29.0.2",
143
150
  "react": "^19.2.5",
144
151
  "react-dom": "^19.2.5",