@classytic/arc-next 0.11.1 → 0.13.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 +1 -1
- package/README.md +57 -0
- package/dist/api.d.ts +71 -117
- package/dist/api.js +16 -2
- package/dist/cache.d.ts +28 -7
- package/dist/cache.js +52 -1
- package/dist/client.d.ts +126 -13
- package/dist/client.js +159 -24
- package/dist/encryption.d.ts +1 -2
- package/dist/hooks.d.ts +32 -29
- package/dist/hooks.js +99 -27
- package/dist/mutation.d.ts +30 -4
- package/dist/mutation.js +28 -11
- package/dist/prefetch.d.ts +0 -1
- package/dist/presets/bulk.d.ts +0 -1
- package/dist/presets/history.d.ts +1 -2
- package/dist/presets/search.d.ts +17 -8
- package/dist/presets/slug.d.ts +0 -1
- package/dist/presets/soft-delete.d.ts +0 -1
- package/dist/presets/tree.d.ts +0 -1
- package/dist/query-client.d.ts +0 -1
- package/dist/query-options.d.ts +32 -26
- package/dist/query.d.ts +7 -47
- package/dist/sse.d.ts +0 -1
- package/dist/sse.js +4 -3
- package/dist/upload.d.ts +9 -1
- package/dist/upload.js +8 -4
- package/dist/ws.d.ts +1 -2
- package/dist/ws.js +1 -1
- package/package.json +19 -8
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"
|
|
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'`,
|
|
@@ -47,6 +46,8 @@ interface ArcApiErrorOptions {
|
|
|
47
46
|
json: unknown;
|
|
48
47
|
endpoint: string;
|
|
49
48
|
method: HttpMethod;
|
|
49
|
+
/** Parsed `Retry-After` response header in ms (429/503 pacing), if present. */
|
|
50
|
+
retryAfterMs?: number | null;
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* Rich API error with status code, response payload, and request metadata.
|
|
@@ -68,6 +69,13 @@ declare class ArcApiError extends Error {
|
|
|
68
69
|
readonly json: unknown;
|
|
69
70
|
readonly endpoint: string;
|
|
70
71
|
readonly method: HttpMethod;
|
|
72
|
+
/**
|
|
73
|
+
* Server-directed retry pacing from the `Retry-After` response header
|
|
74
|
+
* (seconds or HTTP-date form, normalized to ms). Populated on 429/503;
|
|
75
|
+
* the SDK's retry loop honors it INSTEAD of computed backoff so clients
|
|
76
|
+
* come back exactly when the server says capacity returns.
|
|
77
|
+
*/
|
|
78
|
+
readonly retryAfterMs: number | null;
|
|
71
79
|
constructor(message: string, options: ArcApiErrorOptions);
|
|
72
80
|
/**
|
|
73
81
|
* Canonical error code from arc's wire envelope (`json.code`).
|
|
@@ -185,6 +193,43 @@ declare function getQuotaDetails(error: unknown): QuotaDetails | null;
|
|
|
185
193
|
* }
|
|
186
194
|
*/
|
|
187
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;
|
|
188
233
|
/**
|
|
189
234
|
* Specific predicate for validation failures (Fastify AJV + Mongoose
|
|
190
235
|
* ValidationError). When true, `error.fieldErrors` is populated with the
|
|
@@ -240,7 +285,7 @@ interface ClientConfig {
|
|
|
240
285
|
* - 'bearer' (default): Requires a token for authenticated requests. Queries are disabled until a token is provided.
|
|
241
286
|
* - 'cookie': Auth is handled via HTTP-only cookies (e.g. Better Auth). Queries are always enabled — no token needed.
|
|
242
287
|
*/
|
|
243
|
-
authMode?:
|
|
288
|
+
authMode?: "bearer" | "cookie" | "header";
|
|
244
289
|
/**
|
|
245
290
|
* Fetch credentials policy.
|
|
246
291
|
* - 'include': Always send cookies cross-origin (required for cookie-based auth).
|
|
@@ -258,6 +303,13 @@ interface ClientConfig {
|
|
|
258
303
|
* @example '2' // sends Accept-Version: 2
|
|
259
304
|
*/
|
|
260
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;
|
|
261
313
|
/**
|
|
262
314
|
* Auto-generate `Idempotency-Key` header for POST/PUT/PATCH requests.
|
|
263
315
|
* Prevents duplicate mutations on network retries.
|
|
@@ -271,6 +323,17 @@ interface ClientConfig {
|
|
|
271
323
|
* Per-request override: set `elevated: true | false` on `RequestOptions`.
|
|
272
324
|
*/
|
|
273
325
|
elevated?: boolean;
|
|
326
|
+
/**
|
|
327
|
+
* Per-attempt request timeout in ms. When set (> 0), every fetch attempt is
|
|
328
|
+
* aborted after this long and fails with a retryable `TimeoutError` —
|
|
329
|
+
* WITHOUT it, a hung connection pends forever unless the caller wires an
|
|
330
|
+
* `AbortSignal` themselves. Composed with the caller's signal (whichever
|
|
331
|
+
* fires first wins); each retry attempt gets a fresh timeout window.
|
|
332
|
+
* Default: disabled (0) — deliberate, to avoid killing legitimately slow
|
|
333
|
+
* report/export endpoints; set it explicitly (10–30s is typical).
|
|
334
|
+
* Per-request override: `ApiRequestOptions.timeoutMs` (0 disables).
|
|
335
|
+
*/
|
|
336
|
+
timeoutMs?: number;
|
|
274
337
|
/**
|
|
275
338
|
* Network-failure + 5xx retry policy. Off by default — TanStack Query already
|
|
276
339
|
* retries query reads (3× by default), but the SDK's mutation flow and any
|
|
@@ -330,7 +393,7 @@ interface RetryConfig {
|
|
|
330
393
|
* - `'linear'`: `300 * (n + 1)` ms
|
|
331
394
|
* - `(attempt) => number`: custom delay in ms; receives the 0-indexed retry attempt
|
|
332
395
|
*/
|
|
333
|
-
backoff?:
|
|
396
|
+
backoff?: "exponential" | "linear" | ((attempt: number) => number);
|
|
334
397
|
/**
|
|
335
398
|
* Whitelist of statuses to retry on, OR a predicate. Defaults to a function
|
|
336
399
|
* that returns true for: any non-`ArcApiError` (network failure / fetch
|
|
@@ -338,6 +401,13 @@ interface RetryConfig {
|
|
|
338
401
|
* AbortError or 4xx.
|
|
339
402
|
*/
|
|
340
403
|
retryOn?: number[] | ((error: unknown) => boolean);
|
|
404
|
+
/**
|
|
405
|
+
* Backoff jitter. `'full'` randomizes each delay uniformly in
|
|
406
|
+
* `[0, computed]` (AWS full-jitter) so a fleet of clients recovering from
|
|
407
|
+
* the same outage doesn't retry in lockstep and re-stampede the backend.
|
|
408
|
+
* Default `'none'` keeps the historical deterministic delays.
|
|
409
|
+
*/
|
|
410
|
+
jitter?: "none" | "full";
|
|
341
411
|
}
|
|
342
412
|
/** Context handed to {@link ClientConfig.beforeRequest}. Mutate + return to override. */
|
|
343
413
|
interface BeforeRequestContext {
|
|
@@ -384,7 +454,7 @@ declare function configureClient(config: ClientConfig): void;
|
|
|
384
454
|
/**
|
|
385
455
|
* Get the configured auth mode. Returns 'bearer' if not configured.
|
|
386
456
|
*/
|
|
387
|
-
declare function getAuthMode():
|
|
457
|
+
declare function getAuthMode(): "bearer" | "cookie" | "header";
|
|
388
458
|
/** Get the configured base URL. Returns empty string if not configured. */
|
|
389
459
|
declare function getBaseUrl(): string;
|
|
390
460
|
/** Whether auto-idempotency is enabled on the global client. */
|
|
@@ -483,7 +553,7 @@ interface AuthErrorContext {
|
|
|
483
553
|
setToken: (token: string | null) => void;
|
|
484
554
|
}
|
|
485
555
|
/** {@link AuthConfig.onAuthError} signature. */
|
|
486
|
-
type AuthErrorHandler = (ctx: AuthErrorContext) => Promise<
|
|
556
|
+
type AuthErrorHandler = (ctx: AuthErrorContext) => Promise<"retry" | "skip">;
|
|
487
557
|
/**
|
|
488
558
|
* Configure auth context for automatic token/orgId injection.
|
|
489
559
|
* When configured, hooks auto-inject these values so you don't need to pass them manually.
|
|
@@ -521,7 +591,7 @@ declare function _resetAuthWarnings(): void;
|
|
|
521
591
|
* "retry without a token" (rare but valid for public endpoints).
|
|
522
592
|
*/
|
|
523
593
|
interface AuthRecoveryResult {
|
|
524
|
-
decision:
|
|
594
|
+
decision: "retry" | "skip";
|
|
525
595
|
overrideToken: string | null | undefined;
|
|
526
596
|
}
|
|
527
597
|
/** @internal — exposed for tests; clears the dedup so they don't bleed. */
|
|
@@ -545,7 +615,7 @@ declare function _getAuthErrorHandler(): {
|
|
|
545
615
|
* as the fetch path — concurrent callers (XHR upload + WebSocket reconnect
|
|
546
616
|
* + SSE probe firing at once) collapse to one refresh.
|
|
547
617
|
*/
|
|
548
|
-
declare function _runAuthRecovery(handler: AuthErrorHandler, ctx: Omit<AuthErrorContext,
|
|
618
|
+
declare function _runAuthRecovery(handler: AuthErrorHandler, ctx: Omit<AuthErrorContext, "setToken">): Promise<AuthRecoveryResult>;
|
|
549
619
|
/**
|
|
550
620
|
* @internal
|
|
551
621
|
* Resolve the next-attempt token. Mirrors the priority in `executeRequest`'s
|
|
@@ -601,10 +671,10 @@ declare function createAuthRefreshHandler(opts: {
|
|
|
601
671
|
* Behavior when the `refresh()` call itself throws. Default: `'skip'`
|
|
602
672
|
* (the original 401 surfaces; consumers handle "session expired" once).
|
|
603
673
|
*/
|
|
604
|
-
onRefreshError?:
|
|
674
|
+
onRefreshError?: "skip" | "throw";
|
|
605
675
|
}): AuthErrorHandler;
|
|
606
676
|
/** Protocol family the URL should target. `http` keeps `getBaseUrl()` as-is; `ws` rewrites `http(s)://` → `ws(s)://`. */
|
|
607
|
-
type StreamUrlProtocol =
|
|
677
|
+
type StreamUrlProtocol = "http" | "ws";
|
|
608
678
|
/**
|
|
609
679
|
* Build an auth-aware URL using the global client + auth singletons.
|
|
610
680
|
*
|
|
@@ -618,7 +688,7 @@ type StreamUrlProtocol = 'http' | 'ws';
|
|
|
618
688
|
* @param protocol `'http'` (default) or `'ws'` — controls the protocol rewrite.
|
|
619
689
|
*/
|
|
620
690
|
declare function buildStreamUrl(path: string, params?: Record<string, string | number | boolean | null | undefined>, protocol?: StreamUrlProtocol): string;
|
|
621
|
-
type HttpMethod =
|
|
691
|
+
type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
622
692
|
/** Returned by `handleApiRequest` for PDF, image, and CSV responses. */
|
|
623
693
|
interface BlobResponse {
|
|
624
694
|
data: Blob;
|
|
@@ -677,6 +747,8 @@ interface ApiRequestOptions {
|
|
|
677
747
|
next?: NextFetchOptions;
|
|
678
748
|
cache?: RequestCache;
|
|
679
749
|
signal?: AbortSignal;
|
|
750
|
+
/** Per-request timeout in ms — overrides `ClientConfig.timeoutMs`. `0` disables for this request. */
|
|
751
|
+
timeoutMs?: number;
|
|
680
752
|
/** Explicit idempotency key for this request. Sent as `Idempotency-Key` header. */
|
|
681
753
|
idempotencyKey?: string;
|
|
682
754
|
/**
|
|
@@ -762,6 +834,47 @@ declare function createClient(config: ArcClientConfig): ArcClient;
|
|
|
762
834
|
* });
|
|
763
835
|
*/
|
|
764
836
|
declare function createAuthAwareClient(overrides?: Partial<ArcClientConfig>): ArcClient;
|
|
837
|
+
/**
|
|
838
|
+
* Request-scoped server client config — static credentials for exactly one
|
|
839
|
+
* request, no module singletons.
|
|
840
|
+
*/
|
|
841
|
+
interface ServerClientConfig extends Omit<ArcClientConfig, "getToken" | "getOrgId" | "toast" | "navigation"> {
|
|
842
|
+
/** Bearer token for this request. `null`/omitted = unauthenticated. */
|
|
843
|
+
token?: string | null;
|
|
844
|
+
/** Tenant/org for this request, sent as `x-organization-id`. */
|
|
845
|
+
organizationId?: string | null;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Create a **request-scoped** client for server environments (Next.js App
|
|
849
|
+
* Router route handlers / Server Components, or any SSR runtime).
|
|
850
|
+
*
|
|
851
|
+
* Unlike `configureClient`/`configureAuth` — which set module-level state
|
|
852
|
+
* that leaks across concurrent server requests — this returns an isolated
|
|
853
|
+
* client whose credentials live only in the returned instance. Construct one
|
|
854
|
+
* per request, read cookies/headers in YOUR framework code, and pass the
|
|
855
|
+
* values in. The SDK stays framework-free: no `next` dependency, no
|
|
856
|
+
* `next/headers` import, no hidden cookie access.
|
|
857
|
+
*
|
|
858
|
+
* @example Next.js App Router (Server Component or route handler)
|
|
859
|
+
* ```ts
|
|
860
|
+
* import { cookies, headers } from 'next/headers'; // host code, not SDK code
|
|
861
|
+
* import { createServerClient } from '@classytic/arc-next/client';
|
|
862
|
+
* import { createCrudApi } from '@classytic/arc-next/api';
|
|
863
|
+
*
|
|
864
|
+
* export async function loadOrders() {
|
|
865
|
+
* const jar = await cookies();
|
|
866
|
+
* const client = createServerClient({
|
|
867
|
+
* baseUrl: process.env.API_URL!,
|
|
868
|
+
* token: jar.get('session')?.value ?? null,
|
|
869
|
+
* organizationId: (await headers()).get('x-organization-id'),
|
|
870
|
+
* });
|
|
871
|
+
* const orders = createCrudApi<Order>('orders', { client });
|
|
872
|
+
* // Next fetch-cache passthrough works per call:
|
|
873
|
+
* return orders.getAll({ options: { next: { revalidate: 60, tags: ['orders'] } } });
|
|
874
|
+
* }
|
|
875
|
+
* ```
|
|
876
|
+
*/
|
|
877
|
+
declare function createServerClient(config: ServerClientConfig): ArcClient;
|
|
765
878
|
/**
|
|
766
879
|
* Get auth context for a specific client instance, falling back to global.
|
|
767
880
|
*/
|
|
@@ -809,7 +922,7 @@ declare function createQueryString<T extends Record<string, unknown>>(params?: T
|
|
|
809
922
|
* });
|
|
810
923
|
*/
|
|
811
924
|
declare function arcAuthHeaders(): Record<string, string>;
|
|
812
|
-
interface ArcFetchOptions extends Omit<RequestInit,
|
|
925
|
+
interface ArcFetchOptions extends Omit<RequestInit, "body" | "headers"> {
|
|
813
926
|
/**
|
|
814
927
|
* Request body. Plain objects and arrays are auto-`JSON.stringify`d and
|
|
815
928
|
* sent with `Content-Type: application/json`. Binary bodies (`FormData`,
|
|
@@ -903,4 +1016,4 @@ declare const arc: {
|
|
|
903
1016
|
delete: <T = unknown>(path: string, opts?: ArcFetchOptions) => Promise<T>;
|
|
904
1017
|
};
|
|
905
1018
|
//#endregion
|
|
906
|
-
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, 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, 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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ERROR_CODES } from "@classytic/repo-core/errors";
|
|
2
|
+
|
|
1
3
|
//#region src/client.ts
|
|
2
4
|
/**
|
|
3
5
|
* Canonical error codes arc and repo-core emit on `json.code`. Single
|
|
@@ -21,17 +23,7 @@
|
|
|
21
23
|
* `errorMappers` codes still satisfy it.
|
|
22
24
|
*/
|
|
23
25
|
const KNOWN_ARC_ERROR_CODES = [
|
|
24
|
-
|
|
25
|
-
"not_found",
|
|
26
|
-
"conflict",
|
|
27
|
-
"unauthorized",
|
|
28
|
-
"forbidden",
|
|
29
|
-
"rate_limited",
|
|
30
|
-
"idempotency_conflict",
|
|
31
|
-
"precondition_failed",
|
|
32
|
-
"internal_error",
|
|
33
|
-
"service_unavailable",
|
|
34
|
-
"timeout",
|
|
26
|
+
...Object.values(ERROR_CODES),
|
|
35
27
|
"arc.bad_request",
|
|
36
28
|
"arc.unauthorized",
|
|
37
29
|
"arc.forbidden",
|
|
@@ -45,6 +37,7 @@ const KNOWN_ARC_ERROR_CODES = [
|
|
|
45
37
|
"arc.gateway_timeout",
|
|
46
38
|
"arc.validation_error",
|
|
47
39
|
"arc.invalid_id",
|
|
40
|
+
"arc.tier_required",
|
|
48
41
|
"arc.org.selection_required",
|
|
49
42
|
"arc.org.access_denied",
|
|
50
43
|
"ORG_CONTEXT_REQUIRED",
|
|
@@ -75,6 +68,13 @@ var ArcApiError = class extends Error {
|
|
|
75
68
|
json;
|
|
76
69
|
endpoint;
|
|
77
70
|
method;
|
|
71
|
+
/**
|
|
72
|
+
* Server-directed retry pacing from the `Retry-After` response header
|
|
73
|
+
* (seconds or HTTP-date form, normalized to ms). Populated on 429/503;
|
|
74
|
+
* the SDK's retry loop honors it INSTEAD of computed backoff so clients
|
|
75
|
+
* come back exactly when the server says capacity returns.
|
|
76
|
+
*/
|
|
77
|
+
retryAfterMs;
|
|
78
78
|
constructor(message, options) {
|
|
79
79
|
super(message);
|
|
80
80
|
this.name = "ArcApiError";
|
|
@@ -83,6 +83,7 @@ var ArcApiError = class extends Error {
|
|
|
83
83
|
this.json = options.json;
|
|
84
84
|
this.endpoint = options.endpoint;
|
|
85
85
|
this.method = options.method;
|
|
86
|
+
this.retryAfterMs = options.retryAfterMs ?? null;
|
|
86
87
|
}
|
|
87
88
|
/**
|
|
88
89
|
* Canonical error code from arc's wire envelope (`json.code`).
|
|
@@ -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
|
|
@@ -435,7 +478,7 @@ function isAuthRecoverable(error, retryOn403) {
|
|
|
435
478
|
function runAuthRecovery(handler, ctx) {
|
|
436
479
|
if (pendingAuthRecovery) return pendingAuthRecovery;
|
|
437
480
|
pendingAuthRecovery = (async () => {
|
|
438
|
-
let overrideToken
|
|
481
|
+
let overrideToken;
|
|
439
482
|
const setToken = (token) => {
|
|
440
483
|
overrideToken = token;
|
|
441
484
|
};
|
|
@@ -652,6 +695,44 @@ function createAuthAwareClient(overrides = {}) {
|
|
|
652
695
|
};
|
|
653
696
|
}
|
|
654
697
|
/**
|
|
698
|
+
* Create a **request-scoped** client for server environments (Next.js App
|
|
699
|
+
* Router route handlers / Server Components, or any SSR runtime).
|
|
700
|
+
*
|
|
701
|
+
* Unlike `configureClient`/`configureAuth` — which set module-level state
|
|
702
|
+
* that leaks across concurrent server requests — this returns an isolated
|
|
703
|
+
* client whose credentials live only in the returned instance. Construct one
|
|
704
|
+
* per request, read cookies/headers in YOUR framework code, and pass the
|
|
705
|
+
* values in. The SDK stays framework-free: no `next` dependency, no
|
|
706
|
+
* `next/headers` import, no hidden cookie access.
|
|
707
|
+
*
|
|
708
|
+
* @example Next.js App Router (Server Component or route handler)
|
|
709
|
+
* ```ts
|
|
710
|
+
* import { cookies, headers } from 'next/headers'; // host code, not SDK code
|
|
711
|
+
* import { createServerClient } from '@classytic/arc-next/client';
|
|
712
|
+
* import { createCrudApi } from '@classytic/arc-next/api';
|
|
713
|
+
*
|
|
714
|
+
* export async function loadOrders() {
|
|
715
|
+
* const jar = await cookies();
|
|
716
|
+
* const client = createServerClient({
|
|
717
|
+
* baseUrl: process.env.API_URL!,
|
|
718
|
+
* token: jar.get('session')?.value ?? null,
|
|
719
|
+
* organizationId: (await headers()).get('x-organization-id'),
|
|
720
|
+
* });
|
|
721
|
+
* const orders = createCrudApi<Order>('orders', { client });
|
|
722
|
+
* // Next fetch-cache passthrough works per call:
|
|
723
|
+
* return orders.getAll({ options: { next: { revalidate: 60, tags: ['orders'] } } });
|
|
724
|
+
* }
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
function createServerClient(config) {
|
|
728
|
+
const { token = null, organizationId = null, ...rest } = config;
|
|
729
|
+
return createClient({
|
|
730
|
+
...rest,
|
|
731
|
+
getToken: () => token,
|
|
732
|
+
getOrgId: () => organizationId
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
655
736
|
* Get auth context for a specific client instance, falling back to global.
|
|
656
737
|
*/
|
|
657
738
|
function getClientAuthContext(client) {
|
|
@@ -668,11 +749,11 @@ function defaultShouldRetry(error) {
|
|
|
668
749
|
return error instanceof Error;
|
|
669
750
|
}
|
|
670
751
|
/** Compute the delay in ms before the Nth retry. */
|
|
671
|
-
function computeBackoff(retry, attempt) {
|
|
752
|
+
function computeBackoff(retry, attempt, error) {
|
|
753
|
+
if (isArcApiError(error) && error.retryAfterMs != null && (error.status === 429 || error.status === 503)) return error.retryAfterMs;
|
|
672
754
|
const strategy = retry.backoff ?? "exponential";
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
return Math.min(300 * Math.pow(2, attempt), 1e4);
|
|
755
|
+
const base = typeof strategy === "function" ? Math.max(0, strategy(attempt)) : strategy === "linear" ? 300 * (attempt + 1) : Math.min(300 * 2 ** attempt, 1e4);
|
|
756
|
+
return retry.jitter === "full" ? Math.random() * base : base;
|
|
676
757
|
}
|
|
677
758
|
/**
|
|
678
759
|
* Inner request loop — handles 5xx + network-failure backoff per
|
|
@@ -695,7 +776,7 @@ async function executeWithBackoff(config, method, endpoint, options = {}) {
|
|
|
695
776
|
} catch (error) {
|
|
696
777
|
lastError = error;
|
|
697
778
|
if (attempt === totalAttempts - 1 || !shouldRetry(error)) throw error;
|
|
698
|
-
const delay = computeBackoff(config.retry ?? {}, attempt);
|
|
779
|
+
const delay = computeBackoff(config.retry ?? {}, attempt, error);
|
|
699
780
|
if (delay > 0) await sleepAbortable(delay, options.signal);
|
|
700
781
|
}
|
|
701
782
|
throw lastError;
|
|
@@ -714,6 +795,14 @@ async function executeWithBackoff(config, method, endpoint, options = {}) {
|
|
|
714
795
|
* surfaces immediately, no extra round-trip).
|
|
715
796
|
*/
|
|
716
797
|
async function executeRequest(config, method, endpoint, options = {}) {
|
|
798
|
+
if (config.autoIdempotency && options.idempotencyKey === void 0 && (method === "POST" || method === "PUT" || method === "PATCH")) options = {
|
|
799
|
+
...options,
|
|
800
|
+
idempotencyKey: globalThis.crypto.randomUUID()
|
|
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
|
+
};
|
|
717
806
|
const handler = authConfig?.onAuthError;
|
|
718
807
|
if (!handler) return executeWithBackoff(config, method, endpoint, options);
|
|
719
808
|
const retryOn403 = authConfig?.retryOn403 ?? false;
|
|
@@ -741,6 +830,15 @@ async function executeRequest(config, method, endpoint, options = {}) {
|
|
|
741
830
|
}
|
|
742
831
|
throw new Error("arc-next: auth retry loop terminated without resolution");
|
|
743
832
|
}
|
|
833
|
+
/** Parse `Retry-After` (delta-seconds or HTTP-date) to ms; null when absent/invalid. */
|
|
834
|
+
function parseRetryAfterMs(header) {
|
|
835
|
+
if (!header) return null;
|
|
836
|
+
const seconds = Number(header);
|
|
837
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1e3);
|
|
838
|
+
const date = Date.parse(header);
|
|
839
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
744
842
|
/** Sleep that resolves early if the signal aborts. */
|
|
745
843
|
function sleepAbortable(ms, signal) {
|
|
746
844
|
return new Promise((resolve, reject) => {
|
|
@@ -759,9 +857,42 @@ function sleepAbortable(ms, signal) {
|
|
|
759
857
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
760
858
|
});
|
|
761
859
|
}
|
|
860
|
+
/**
|
|
861
|
+
* Compose the caller's AbortSignal with a per-attempt timeout. Returns the
|
|
862
|
+
* effective signal plus a `timedOut()` probe so the catch path can tell a
|
|
863
|
+
* timeout abort (retryable failure) apart from a deliberate caller abort
|
|
864
|
+
* (silent cancellation).
|
|
865
|
+
*/
|
|
866
|
+
function withTimeoutSignal(signal, timeoutMs) {
|
|
867
|
+
if (!(timeoutMs > 0)) return {
|
|
868
|
+
signal,
|
|
869
|
+
timedOut: () => false,
|
|
870
|
+
cleanup: () => {}
|
|
871
|
+
};
|
|
872
|
+
const controller = new AbortController();
|
|
873
|
+
let timedOut = false;
|
|
874
|
+
const onCallerAbort = () => controller.abort();
|
|
875
|
+
if (signal) if (signal.aborted) controller.abort();
|
|
876
|
+
else signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
877
|
+
const timer = setTimeout(() => {
|
|
878
|
+
timedOut = true;
|
|
879
|
+
controller.abort();
|
|
880
|
+
}, timeoutMs);
|
|
881
|
+
return {
|
|
882
|
+
signal: controller.signal,
|
|
883
|
+
timedOut: () => timedOut,
|
|
884
|
+
cleanup: () => {
|
|
885
|
+
clearTimeout(timer);
|
|
886
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
762
890
|
/** A single fetch attempt — used by executeRequest's retry loop. */
|
|
763
891
|
async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
764
|
-
const { body, token, organizationId, revalidate, headerOptions, tags, next, cache, signal, idempotencyKey, elevated } = options;
|
|
892
|
+
const { body, token, organizationId, revalidate, headerOptions, tags, next, cache, signal: callerSignal, idempotencyKey, elevated } = options;
|
|
893
|
+
const timeoutMs = options.timeoutMs ?? config.timeoutMs ?? 0;
|
|
894
|
+
const timeout = withTimeoutSignal(callerSignal, timeoutMs);
|
|
895
|
+
const signal = timeout.signal;
|
|
765
896
|
const startTime = Date.now();
|
|
766
897
|
try {
|
|
767
898
|
let headers = {
|
|
@@ -773,7 +904,7 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
773
904
|
if (config.authMode === "header") {
|
|
774
905
|
const headerName = authConfig?.headerName ?? "x-api-key";
|
|
775
906
|
headers[headerName] = token;
|
|
776
|
-
} else if (config.authMode !== "cookie") headers
|
|
907
|
+
} else if (config.authMode !== "cookie") headers.Authorization = `Bearer ${token}`;
|
|
777
908
|
}
|
|
778
909
|
if (config.apiVersion) headers["Accept-Version"] = config.apiVersion;
|
|
779
910
|
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
|
|
@@ -784,7 +915,7 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
784
915
|
...headerOptions
|
|
785
916
|
};
|
|
786
917
|
const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
|
|
787
|
-
let serializedBody
|
|
918
|
+
let serializedBody;
|
|
788
919
|
if (body !== void 0 && body !== null) serializedBody = isNonJsonBody(body) ? body : JSON.stringify(body);
|
|
789
920
|
const encryption = config.encryption;
|
|
790
921
|
if (encryption?.encryptRequests && encryption.encrypt && typeof serializedBody === "string") {
|
|
@@ -815,7 +946,7 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
815
946
|
if (revalidate !== void 0) nextCfg.revalidate = revalidate;
|
|
816
947
|
if (tags) nextCfg.tags = nextCfg.tags ? [...nextCfg.tags, ...tags] : tags;
|
|
817
948
|
if (nextCfg.revalidate !== void 0 || nextCfg.tags) fetchOptions.next = nextCfg;
|
|
818
|
-
if (!/^https?:\/\//i.test(endpoint) && !config.baseUrl) throw new Error(`[arc-next] handleApiRequest(${method} ${endpoint}): baseUrl is empty.
|
|
949
|
+
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."));
|
|
819
950
|
const response = await fetch(`${config.baseUrl}${endpoint}`, fetchOptions);
|
|
820
951
|
if (!response.ok) {
|
|
821
952
|
let json = null;
|
|
@@ -838,7 +969,8 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
838
969
|
statusText: response.statusText,
|
|
839
970
|
json,
|
|
840
971
|
endpoint,
|
|
841
|
-
method
|
|
972
|
+
method,
|
|
973
|
+
retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After"))
|
|
842
974
|
});
|
|
843
975
|
}
|
|
844
976
|
const contentType = response.headers.get("Content-Type");
|
|
@@ -886,8 +1018,11 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
886
1018
|
})).body;
|
|
887
1019
|
return data;
|
|
888
1020
|
} catch (error) {
|
|
1021
|
+
if (timeout.timedOut() && isAbortError(error)) throw Object.assign(/* @__PURE__ */ new Error(`[arc-next] Request timed out after ${timeoutMs}ms: ${method} ${endpoint}`), { name: "TimeoutError" });
|
|
889
1022
|
if (error instanceof Error) throw error;
|
|
890
1023
|
throw new Error("An error occurred while fetching data.");
|
|
1024
|
+
} finally {
|
|
1025
|
+
timeout.cleanup();
|
|
891
1026
|
}
|
|
892
1027
|
}
|
|
893
1028
|
/**
|
|
@@ -1017,7 +1152,7 @@ function arcAuthHeaders() {
|
|
|
1017
1152
|
* Lower-cased on lookup so case-insensitive HTTP header semantics are
|
|
1018
1153
|
* honored (`Authorization` vs `authorization` both protected).
|
|
1019
1154
|
*/
|
|
1020
|
-
const ARC_FETCH_PROTECTED_HEADERS = new Set([
|
|
1155
|
+
const ARC_FETCH_PROTECTED_HEADERS = /* @__PURE__ */ new Set([
|
|
1021
1156
|
"authorization",
|
|
1022
1157
|
"x-organization-id",
|
|
1023
1158
|
"x-internal-api-key"
|
|
@@ -1139,4 +1274,4 @@ const arc = {
|
|
|
1139
1274
|
};
|
|
1140
1275
|
|
|
1141
1276
|
//#endregion
|
|
1142
|
-
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, getQuotaDetails, handleApiRequest, hasGlobalStaticAuth, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isQuotaExceeded, isValidationError };
|
|
1277
|
+
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 };
|
package/dist/encryption.d.ts
CHANGED
|
@@ -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;
|
|
@@ -31,6 +30,6 @@ interface JoseEncryptionOptions {
|
|
|
31
30
|
* using `jose`. Spread the result into `encryption` and add `encryptRequests`
|
|
32
31
|
* / content-type overrides as needed.
|
|
33
32
|
*/
|
|
34
|
-
declare function createJoseEncryption(options: JoseEncryptionOptions): Pick<ClientEncryptionConfig,
|
|
33
|
+
declare function createJoseEncryption(options: JoseEncryptionOptions): Pick<ClientEncryptionConfig, "encrypt" | "decrypt">;
|
|
35
34
|
//#endregion
|
|
36
35
|
export { JoseEncryptionOptions, createJoseEncryption };
|