next-sanctum 0.1.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/index.cjs ADDED
@@ -0,0 +1,1395 @@
1
+ 'use client';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var react = require('react');
6
+
7
+ /**
8
+ * Error types for next-sanctum. All failures are normalized to `SanctumError`
9
+ * so consumers can handle them consistently (see plan §10: errors must not leak).
10
+ */ /** Base error for all module failures. */ class SanctumError extends Error {
11
+ constructor(message, options){
12
+ super(message, {
13
+ cause: options.cause
14
+ });
15
+ this.name = "SanctumError";
16
+ this.kind = options.kind;
17
+ this.status = options.status;
18
+ this.data = options.data;
19
+ }
20
+ }
21
+ /** Invalid configuration — fail-fast on init (see resolveConfig). */ class ConfigError extends SanctumError {
22
+ constructor(message, cause){
23
+ super(message, {
24
+ kind: "config",
25
+ cause
26
+ });
27
+ this.name = "ConfigError";
28
+ }
29
+ }
30
+ /**
31
+ * HTTP 422 from Laravel. Exposes field errors (`{ field: string[] }`) so
32
+ * consumers can map them to their forms.
33
+ */ class ValidationError extends SanctumError {
34
+ constructor(message, errors, data){
35
+ super(message, {
36
+ kind: "validation",
37
+ status: 422,
38
+ data
39
+ });
40
+ this.name = "ValidationError";
41
+ this.errors = errors;
42
+ }
43
+ }
44
+ function isRecord(value) {
45
+ return typeof value === "object" && value !== null;
46
+ }
47
+ /** Try to read the response body as JSON; return undefined if it isn't JSON. */ async function readBody(response) {
48
+ const contentType = response.headers.get("content-type") ?? "";
49
+ try {
50
+ if (contentType.includes("application/json")) {
51
+ return await response.json();
52
+ }
53
+ const text = await response.text();
54
+ return text.length > 0 ? text : undefined;
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ function kindForStatus(status) {
60
+ switch(status){
61
+ case 401:
62
+ return "unauthorized";
63
+ case 403:
64
+ return "forbidden";
65
+ case 419:
66
+ return "csrf";
67
+ case 422:
68
+ return "validation";
69
+ default:
70
+ return "http";
71
+ }
72
+ }
73
+ /**
74
+ * Build a `SanctumError` from a non-2xx Response. The message is taken from the
75
+ * Laravel body (`message`) when present, without leaking the stack or internal details.
76
+ */ async function errorFromResponse(response) {
77
+ const data = await readBody(response);
78
+ const body = isRecord(data) ? data : {};
79
+ const message = body.message ?? `Request failed with status ${response.status}.`;
80
+ if (response.status === 422) {
81
+ return new ValidationError(message, body.errors ?? {}, data);
82
+ }
83
+ return new SanctumError(message, {
84
+ kind: kindForStatus(response.status),
85
+ status: response.status,
86
+ data
87
+ });
88
+ }
89
+ /** Wrap a network error (fetch rejection) into a SanctumError. */ function networkError(cause) {
90
+ const message = cause instanceof Error ? cause.message : "Network request failed.";
91
+ return new SanctumError(message, {
92
+ kind: "network",
93
+ cause
94
+ });
95
+ }
96
+
97
+ /** Typed lifecycle emitter (see SanctumEventMap). */ class SanctumEventEmitter {
98
+ on(event, handler) {
99
+ let set = this.handlers.get(event);
100
+ if (!set) {
101
+ set = new Set();
102
+ this.handlers.set(event, set);
103
+ }
104
+ const fn = handler;
105
+ set.add(fn);
106
+ return ()=>{
107
+ set.delete(fn);
108
+ };
109
+ }
110
+ emit(event, payload) {
111
+ const set = this.handlers.get(event);
112
+ if (!set) return;
113
+ // Snapshot + isolate: a throwing consumer handler must not break the auth/request
114
+ // flow this is emitted from, nor skip the remaining handlers.
115
+ for (const handler of [
116
+ ...set
117
+ ]){
118
+ try {
119
+ handler(payload);
120
+ } catch {
121
+ // swallow consumer handler errors
122
+ }
123
+ }
124
+ }
125
+ /** Register many handlers at once (used by the provider from config.events). */ register(handlers) {
126
+ for (const key of Object.keys(handlers)){
127
+ const handler = handlers[key];
128
+ if (!handler) continue;
129
+ let set = this.handlers.get(key);
130
+ if (!set) {
131
+ set = new Set();
132
+ this.handlers.set(key, set);
133
+ }
134
+ set.add(handler);
135
+ }
136
+ }
137
+ constructor(){
138
+ this.handlers = new Map();
139
+ }
140
+ }
141
+
142
+ const PREFIX = "[next-sanctum]";
143
+ /** Leveled logger. logLevel 0 = silent; 3 = info (default). */ function createLogger(level) {
144
+ const enabled = (threshold)=>level >= threshold;
145
+ return {
146
+ error: (...args)=>{
147
+ if (enabled(1)) console.error(PREFIX, ...args);
148
+ },
149
+ warn: (...args)=>{
150
+ if (enabled(2)) console.warn(PREFIX, ...args);
151
+ },
152
+ info: (...args)=>{
153
+ if (enabled(3)) console.info(PREFIX, ...args);
154
+ },
155
+ debug: (...args)=>{
156
+ if (enabled(4)) console.debug(PREFIX, ...args);
157
+ }
158
+ };
159
+ }
160
+
161
+ const DEFAULT_ENDPOINTS = {
162
+ csrf: "/sanctum/csrf-cookie",
163
+ login: "/login",
164
+ logout: "/logout",
165
+ user: "/api/user",
166
+ register: "/register",
167
+ forgotPassword: "/forgot-password",
168
+ resetPassword: "/reset-password",
169
+ emailVerificationNotification: "/email/verification-notification",
170
+ verifyEmail: "/email/verify",
171
+ confirmPassword: "/user/confirm-password",
172
+ confirmedPasswordStatus: "/user/confirmed-password-status",
173
+ profileInformation: "/user/profile-information",
174
+ updatePassword: "/user/password",
175
+ twoFactor: {
176
+ challenge: "/two-factor-challenge",
177
+ enable: "/user/two-factor-authentication",
178
+ confirm: "/user/confirmed-two-factor-authentication",
179
+ disable: "/user/two-factor-authentication",
180
+ qrCode: "/user/two-factor-qr-code",
181
+ secretKey: "/user/two-factor-secret-key",
182
+ recoveryCodes: "/user/two-factor-recovery-codes"
183
+ },
184
+ passkeys: {
185
+ loginOptions: "/passkeys/login/options",
186
+ login: "/passkeys/login",
187
+ confirmOptions: "/passkeys/confirm/options",
188
+ confirm: "/passkeys/confirm",
189
+ registerOptions: "/user/passkeys/options",
190
+ register: "/user/passkeys",
191
+ delete: "/user/passkeys"
192
+ },
193
+ sessions: {
194
+ list: "/api/sessions",
195
+ logoutOthers: "/api/sessions/others",
196
+ logout: "/api/sessions"
197
+ }
198
+ };
199
+ function resolveTwoFactor(value) {
200
+ if (value === false) return false;
201
+ if (value === undefined || value === true) {
202
+ return {
203
+ confirm: true,
204
+ confirmPassword: true
205
+ };
206
+ }
207
+ return {
208
+ confirm: value.confirm ?? true,
209
+ confirmPassword: value.confirmPassword ?? true
210
+ };
211
+ }
212
+ function resolvePasskeys(value) {
213
+ if (value === undefined || value === false) return false;
214
+ if (value === true) return {
215
+ confirmPassword: true
216
+ };
217
+ return {
218
+ confirmPassword: value.confirmPassword ?? true
219
+ };
220
+ }
221
+ function defaultOrigin(input) {
222
+ if (input) return input;
223
+ if (typeof window !== "undefined") return window.location.origin;
224
+ return undefined;
225
+ }
226
+ function resolveFetch(input) {
227
+ const impl = input ?? globalThis.fetch;
228
+ if (typeof impl !== "function") {
229
+ throw new ConfigError("Native `fetch` is not available. Provide `config.fetch` (Node < 18) or use a modern runtime.");
230
+ }
231
+ return impl.bind(globalThis);
232
+ }
233
+ /**
234
+ * Validate + fill in config defaults. Fail-fast when `baseUrl` is missing to avoid
235
+ * an SSR loop / fetching the URL `undefined` (see plan §10 risks).
236
+ */ function resolveConfig(input) {
237
+ if (!input || typeof input.baseUrl !== "string" || input.baseUrl.trim() === "") {
238
+ throw new ConfigError("`baseUrl` is required (the Laravel API URL, e.g. https://api.domain.com).");
239
+ }
240
+ const features = input.features ?? {};
241
+ const endpoints = input.endpoints ?? {};
242
+ return {
243
+ baseUrl: input.baseUrl.replace(/\/+$/, ""),
244
+ mode: input.mode ?? "cookie",
245
+ origin: defaultOrigin(input.origin),
246
+ features: {
247
+ registration: features.registration ?? true,
248
+ resetPasswords: features.resetPasswords ?? true,
249
+ emailVerification: features.emailVerification ?? true,
250
+ updateProfileInformation: features.updateProfileInformation ?? true,
251
+ updatePasswords: features.updatePasswords ?? true,
252
+ deviceSessions: features.deviceSessions ?? false,
253
+ twoFactorAuthentication: resolveTwoFactor(features.twoFactorAuthentication),
254
+ passkeys: resolvePasskeys(features.passkeys)
255
+ },
256
+ endpoints: {
257
+ ...DEFAULT_ENDPOINTS,
258
+ ...endpoints,
259
+ twoFactor: {
260
+ ...DEFAULT_ENDPOINTS.twoFactor,
261
+ ...endpoints.twoFactor
262
+ },
263
+ passkeys: {
264
+ ...DEFAULT_ENDPOINTS.passkeys,
265
+ ...endpoints.passkeys
266
+ },
267
+ sessions: {
268
+ ...DEFAULT_ENDPOINTS.sessions,
269
+ ...endpoints.sessions
270
+ }
271
+ },
272
+ csrf: {
273
+ cookie: input.csrf?.cookie ?? "XSRF-TOKEN",
274
+ header: input.csrf?.header ?? "X-XSRF-TOKEN"
275
+ },
276
+ redirect: {
277
+ onLogin: input.redirect?.onLogin ?? "/",
278
+ onLogout: input.redirect?.onLogout ?? "/",
279
+ onAuthOnly: input.redirect?.onAuthOnly ?? "/login",
280
+ onGuestOnly: input.redirect?.onGuestOnly ?? "/",
281
+ keepRequestedRoute: input.redirect?.keepRequestedRoute ?? false
282
+ },
283
+ logLevel: input.logLevel ?? 3,
284
+ initialRequest: input.initialRequest ?? true,
285
+ retryOnCsrfMismatch: input.retryOnCsrfMismatch ?? true,
286
+ storage: input.storage,
287
+ interceptors: {
288
+ request: input.interceptors?.request ?? [],
289
+ response: input.interceptors?.response ?? []
290
+ },
291
+ events: input.events ?? {},
292
+ redirectIfUnauthenticated: input.redirectIfUnauthenticated ?? false,
293
+ fetch: resolveFetch(input.fetch)
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Open-redirect protection. Only allows SAME-ORIGIN destinations (a relative path
299
+ * or an absolute URL with the same origin), with an optional path allowlist.
300
+ * See PRD §12.1 (Open redirect) & §18 (a unit test is required).
301
+ */ /** Reject backslashes and any control character (Tab/LF/CR are stripped by the URL
302
+ * parser, which could turn `/\t/evil.com` into `//evil.com` and bypass the `//` guard). */ function hasUnsafeChars(value) {
303
+ for(let i = 0; i < value.length; i++){
304
+ const code = value.charCodeAt(i);
305
+ if (code <= 0x1f || code === 0x7f) return true;
306
+ if (value[i] === "\\") return true;
307
+ }
308
+ return false;
309
+ }
310
+ /**
311
+ * Return `target` when it is safe (same-origin), otherwise `fallback`.
312
+ * Rejects: `//evil.com`, `https://evil.com`, the `javascript:` scheme, control-char
313
+ * injection (`/\t//evil.com`), and backslash tricks.
314
+ */ function safeRedirect(target, fallback, options = {}) {
315
+ if (!target) return fallback;
316
+ const trimmed = target.trim();
317
+ if (trimmed === "") return fallback;
318
+ if (hasUnsafeChars(trimmed)) return fallback;
319
+ let path = null;
320
+ if (trimmed.startsWith("/")) {
321
+ // Reject protocol-relative (//evil.com).
322
+ if (trimmed.startsWith("//")) return fallback;
323
+ path = trimmed;
324
+ } else if (options.origin) {
325
+ try {
326
+ const url = new URL(trimmed);
327
+ const base = new URL(options.origin);
328
+ if (url.origin === base.origin) {
329
+ path = url.pathname + url.search + url.hash;
330
+ }
331
+ } catch {
332
+ // not a valid URL → reject
333
+ }
334
+ }
335
+ if (path === null) return fallback;
336
+ // Defense-in-depth: resolve the path and confirm it stays same-origin.
337
+ try {
338
+ const base = options.origin ?? "https://sanctum.invalid";
339
+ if (new URL(path, base).origin !== new URL(base).origin) return fallback;
340
+ } catch {
341
+ return fallback;
342
+ }
343
+ if (options.allowList && options.allowList.length > 0) {
344
+ const safe = path;
345
+ const allowed = options.allowList.some((prefix)=>safe === prefix || safe.startsWith(prefix));
346
+ if (!allowed) return fallback;
347
+ }
348
+ return path;
349
+ }
350
+
351
+ /** HTTP methods that require CSRF protection in cookie mode. */ const STATEFUL_METHODS = new Set([
352
+ "POST",
353
+ "PUT",
354
+ "PATCH",
355
+ "DELETE"
356
+ ]);
357
+ /** Join baseUrl + path. Absolute paths (http/https) are passed through as-is. */ function joinUrl(baseUrl, path) {
358
+ if (/^https?:\/\//i.test(path)) return path;
359
+ const base = baseUrl.replace(/\/+$/, "");
360
+ const suffix = path.startsWith("/") ? path : `/${path}`;
361
+ return base + suffix;
362
+ }
363
+ /**
364
+ * Parse the response body as JSON. Returns `undefined` (cast to T)
365
+ * for 204 / an empty body so the caller doesn't need a manual try/catch.
366
+ */ async function parseJson(response) {
367
+ if (response.status === 204) return undefined;
368
+ const text = await response.text();
369
+ if (text.length === 0) return undefined;
370
+ try {
371
+ return JSON.parse(text);
372
+ } catch (cause) {
373
+ throw new SanctumError("Failed to parse the JSON response body.", {
374
+ kind: "unknown",
375
+ status: response.status,
376
+ cause
377
+ });
378
+ }
379
+ }
380
+
381
+ /**
382
+ * CSRF cookie reading (browser). Sanctum stores `XSRF-TOKEN` in URL-encoded
383
+ * form, so its value MUST be decoded before being sent as the
384
+ * `X-XSRF-TOKEN` header (the most common source of Sanctum integration bugs).
385
+ */ /** Read a single cookie from document.cookie. Null on the server (no document). */ function readCookie(name) {
386
+ if (typeof document === "undefined") return null;
387
+ const cookies = document.cookie ? document.cookie.split("; ") : [];
388
+ for (const cookie of cookies){
389
+ const eq = cookie.indexOf("=");
390
+ const key = eq === -1 ? cookie : cookie.slice(0, eq);
391
+ if (key === name) {
392
+ return eq === -1 ? "" : cookie.slice(eq + 1);
393
+ }
394
+ }
395
+ return null;
396
+ }
397
+ /** Read the XSRF token from the cookie, then URL-decode it. */ function readXsrfToken(cookieName) {
398
+ const raw = readCookie(cookieName);
399
+ if (raw === null) return null;
400
+ try {
401
+ return decodeURIComponent(raw);
402
+ } catch {
403
+ return raw;
404
+ }
405
+ }
406
+
407
+ /**
408
+ * The native-fetch core used by every feature: attaches CSRF (cookie) or
409
+ * Bearer (token), credentials, base URL, interceptors, a single 419 retry, and
410
+ * normalizes errors.
411
+ */ function createSanctumClient(config, deps = {}) {
412
+ const fetchImpl = config.fetch;
413
+ const { logger, emitter } = deps;
414
+ async function fetchCsrfCookie() {
415
+ const url = joinUrl(config.baseUrl, config.endpoints.csrf);
416
+ logger?.debug("GET", url);
417
+ try {
418
+ await fetchImpl(url, {
419
+ method: "GET",
420
+ credentials: "include",
421
+ headers: {
422
+ accept: "application/json"
423
+ }
424
+ });
425
+ } catch (cause) {
426
+ throw networkError(cause);
427
+ }
428
+ }
429
+ // De-duplicate concurrent CSRF-cookie fetches so parallel stateful requests on a
430
+ // fresh page don't each fire (and race) a `GET /sanctum/csrf-cookie`.
431
+ let csrfInFlight = null;
432
+ async function ensureCsrf(force = false) {
433
+ if (config.mode !== "cookie") return;
434
+ if (!force && readXsrfToken(config.csrf.cookie)) return;
435
+ if (!force && csrfInFlight) return csrfInFlight;
436
+ const pending = fetchCsrfCookie().finally(()=>{
437
+ if (csrfInFlight === pending) csrfInFlight = null;
438
+ });
439
+ csrfInFlight = pending;
440
+ return pending;
441
+ }
442
+ async function buildRequest(path, init) {
443
+ const url = joinUrl(config.baseUrl, path);
444
+ const method = (init.method ?? "GET").toUpperCase();
445
+ const headers = new Headers(init.headers);
446
+ if (!headers.has("accept")) headers.set("accept", "application/json");
447
+ const { json, body: rawBody, ...rest } = init;
448
+ let body = rawBody ?? null;
449
+ if (json !== undefined) {
450
+ body = JSON.stringify(json);
451
+ if (!headers.has("content-type")) {
452
+ headers.set("content-type", "application/json");
453
+ }
454
+ }
455
+ if (config.mode === "cookie") {
456
+ const token = readXsrfToken(config.csrf.cookie);
457
+ if (token && !headers.has(config.csrf.header)) {
458
+ headers.set(config.csrf.header, token);
459
+ }
460
+ } else if (deps.getToken) {
461
+ const token = await deps.getToken();
462
+ if (token && !headers.has("authorization")) {
463
+ headers.set("authorization", `Bearer ${token}`);
464
+ }
465
+ }
466
+ const credentials = config.mode === "cookie" ? "include" : init.credentials ?? "same-origin";
467
+ let request = new Request(url, {
468
+ ...rest,
469
+ method,
470
+ headers,
471
+ body,
472
+ credentials
473
+ });
474
+ for (const interceptor of config.interceptors.request){
475
+ request = await interceptor(request);
476
+ }
477
+ return request;
478
+ }
479
+ async function send(path, init, isRetry) {
480
+ const request = await buildRequest(path, init);
481
+ emitter?.emit("request", {
482
+ url: request.url,
483
+ init
484
+ });
485
+ let response;
486
+ try {
487
+ response = await fetchImpl(request);
488
+ } catch (cause) {
489
+ const error = networkError(cause);
490
+ emitter?.emit("error", {
491
+ error
492
+ });
493
+ throw error;
494
+ }
495
+ for (const interceptor of config.interceptors.response){
496
+ response = await interceptor(response, request);
497
+ }
498
+ emitter?.emit("response", {
499
+ url: request.url,
500
+ response
501
+ });
502
+ if (response.status === 419 && config.mode === "cookie" && config.retryOnCsrfMismatch && !isRetry) {
503
+ logger?.warn("CSRF mismatch (419) — refresh token & retry once");
504
+ await ensureCsrf(true);
505
+ return send(path, init, true);
506
+ }
507
+ if (!response.ok) {
508
+ const error = await errorFromResponse(response);
509
+ emitter?.emit("error", {
510
+ error
511
+ });
512
+ throw error;
513
+ }
514
+ return response;
515
+ }
516
+ async function raw(path, init = {}) {
517
+ const method = (init.method ?? "GET").toUpperCase();
518
+ if (config.mode === "cookie" && STATEFUL_METHODS.has(method)) {
519
+ await ensureCsrf();
520
+ }
521
+ return send(path, init, false);
522
+ }
523
+ async function request(path, init = {}) {
524
+ const response = await raw(path, init);
525
+ return parseJson(response);
526
+ }
527
+ return {
528
+ config,
529
+ ensureCsrf,
530
+ raw,
531
+ request
532
+ };
533
+ }
534
+
535
+ /**
536
+ * Core auth API (framework-agnostic). The React provider wraps it to
537
+ * manage state. Login detects `two_factor` BEFORE fetching the user
538
+ * so consumers cannot forget to handle 2FA (discriminated result).
539
+ */ function createAuthApi(client, config, deps = {}) {
540
+ const { emitter } = deps;
541
+ async function refreshIdentity() {
542
+ try {
543
+ const user = await client.request(config.endpoints.user, {
544
+ method: "GET"
545
+ });
546
+ const resolved = user ?? null;
547
+ emitter?.emit("refresh", {
548
+ user: resolved
549
+ });
550
+ return resolved;
551
+ } catch (error) {
552
+ if (error instanceof SanctumError && error.kind === "unauthorized") {
553
+ emitter?.emit("refresh", {
554
+ user: null
555
+ });
556
+ return null;
557
+ }
558
+ throw error;
559
+ }
560
+ }
561
+ async function login(credentials) {
562
+ const data = await client.request(config.endpoints.login, {
563
+ method: "POST",
564
+ json: credentials
565
+ });
566
+ if (data?.two_factor) {
567
+ // Token mode + 2FA is not completable via the standard Fortify flow: the
568
+ // `/two-factor-challenge` endpoint establishes a session and returns no token.
569
+ // Fail loud instead of leaving the user silently unauthenticated.
570
+ if (config.mode === "token") {
571
+ throw new ConfigError("Two-factor authentication during login is only supported in cookie mode. " + "In token mode, mint the token from a 2FA-aware endpoint.");
572
+ }
573
+ emitter?.emit("two-factor-required", {});
574
+ return {
575
+ status: "two-factor-required"
576
+ };
577
+ }
578
+ if (config.mode === "token" && data?.token) {
579
+ await deps.setToken?.(data.token);
580
+ }
581
+ const user = await refreshIdentity();
582
+ if (!user) {
583
+ throw new SanctumError("Login succeeded but failed to fetch the user data. Check the user endpoint & configuration.", {
584
+ kind: "unknown"
585
+ });
586
+ }
587
+ emitter?.emit("login", {
588
+ user
589
+ });
590
+ return {
591
+ status: "authenticated",
592
+ user
593
+ };
594
+ }
595
+ async function logout() {
596
+ try {
597
+ await client.raw(config.endpoints.logout, {
598
+ method: "POST"
599
+ });
600
+ } finally{
601
+ if (config.mode === "token") await deps.clearToken?.();
602
+ emitter?.emit("logout", {});
603
+ }
604
+ }
605
+ return {
606
+ login,
607
+ logout,
608
+ refreshIdentity
609
+ };
610
+ }
611
+
612
+ /** Registration (Fortify `POST /register`). On success → a login session is created by the backend. */ function createRegistrationApi(client, config) {
613
+ return {
614
+ async register (payload) {
615
+ if (!config.features.registration) {
616
+ throw new ConfigError('The "registration" feature is disabled in config.');
617
+ }
618
+ await client.raw(config.endpoints.register, {
619
+ method: "POST",
620
+ json: payload
621
+ });
622
+ }
623
+ };
624
+ }
625
+
626
+ function createPasswordApi(client, config) {
627
+ const ep = config.endpoints;
628
+ return {
629
+ async forgotPassword (payload) {
630
+ await client.raw(ep.forgotPassword, {
631
+ method: "POST",
632
+ json: payload
633
+ });
634
+ },
635
+ async resetPassword (payload) {
636
+ await client.raw(ep.resetPassword, {
637
+ method: "POST",
638
+ json: payload
639
+ });
640
+ },
641
+ async confirmPassword (payload) {
642
+ await client.raw(ep.confirmPassword, {
643
+ method: "POST",
644
+ json: payload
645
+ });
646
+ },
647
+ async confirmedPasswordStatus () {
648
+ const data = await client.request(ep.confirmedPasswordStatus, {
649
+ method: "GET"
650
+ });
651
+ return Boolean(data?.confirmed);
652
+ },
653
+ async updatePassword (payload) {
654
+ await client.raw(ep.updatePassword, {
655
+ method: "PUT",
656
+ json: payload
657
+ });
658
+ }
659
+ };
660
+ }
661
+
662
+ function createProfileApi(client, config) {
663
+ return {
664
+ async updateProfileInformation (payload) {
665
+ await client.raw(config.endpoints.profileInformation, {
666
+ method: "PUT",
667
+ json: payload
668
+ });
669
+ }
670
+ };
671
+ }
672
+
673
+ function createEmailVerificationApi(client, config) {
674
+ return {
675
+ async resendEmailVerification () {
676
+ await client.raw(config.endpoints.emailVerificationNotification, {
677
+ method: "POST"
678
+ });
679
+ },
680
+ async verifyEmail (payload) {
681
+ const query = new URLSearchParams({
682
+ expires: String(payload.expires),
683
+ signature: payload.signature
684
+ }).toString();
685
+ const path = `${config.endpoints.verifyEmail}/${encodeURIComponent(String(payload.id))}/${encodeURIComponent(payload.hash)}?${query}`;
686
+ await client.request(path, {
687
+ method: "GET"
688
+ });
689
+ }
690
+ };
691
+ }
692
+
693
+ function createTwoFactorApi(client, config) {
694
+ const ep = config.endpoints.twoFactor;
695
+ function ensureEnabled() {
696
+ if (config.features.twoFactorAuthentication === false) {
697
+ throw new ConfigError('The "twoFactorAuthentication" feature is disabled in config.');
698
+ }
699
+ }
700
+ return {
701
+ async challenge (payload) {
702
+ // Challenge may run even when 2FA management is disabled (login flow).
703
+ await client.raw(ep.challenge, {
704
+ method: "POST",
705
+ json: payload
706
+ });
707
+ },
708
+ async enable () {
709
+ ensureEnabled();
710
+ await client.raw(ep.enable, {
711
+ method: "POST"
712
+ });
713
+ },
714
+ async confirm (code) {
715
+ ensureEnabled();
716
+ await client.raw(ep.confirm, {
717
+ method: "POST",
718
+ json: {
719
+ code
720
+ }
721
+ });
722
+ },
723
+ async disable () {
724
+ ensureEnabled();
725
+ await client.raw(ep.disable, {
726
+ method: "DELETE"
727
+ });
728
+ },
729
+ async getQrCode () {
730
+ ensureEnabled();
731
+ return client.request(ep.qrCode, {
732
+ method: "GET"
733
+ });
734
+ },
735
+ async getSecretKey () {
736
+ ensureEnabled();
737
+ return client.request(ep.secretKey, {
738
+ method: "GET"
739
+ });
740
+ },
741
+ async getRecoveryCodes () {
742
+ ensureEnabled();
743
+ return client.request(ep.recoveryCodes, {
744
+ method: "GET"
745
+ });
746
+ },
747
+ async regenerateRecoveryCodes () {
748
+ ensureEnabled();
749
+ await client.raw(ep.recoveryCodes, {
750
+ method: "POST"
751
+ });
752
+ }
753
+ };
754
+ }
755
+
756
+ async function loadPasskeys() {
757
+ try {
758
+ const mod = await import('@laravel/passkeys');
759
+ return mod.Passkeys;
760
+ } catch (cause) {
761
+ throw new ConfigError("The @laravel/passkeys package is not installed. Run: pnpm add @laravel/passkeys", cause);
762
+ }
763
+ }
764
+ /**
765
+ * Passkeys interop (Fortify). The WebAuthn ceremony is delegated to @laravel/passkeys
766
+ * (dynamic import, browser-only, optional peer). We map the endpoints from config
767
+ * & set credentials/CSRF so the request is authenticated.
768
+ */ function createPasskeysApi(client, config) {
769
+ const ep = config.endpoints.passkeys;
770
+ const abs = (path)=>joinUrl(config.baseUrl, path);
771
+ function ensureEnabled() {
772
+ if (config.features.passkeys === false) {
773
+ throw new ConfigError('The "passkeys" feature is disabled in config.');
774
+ }
775
+ }
776
+ async function configured() {
777
+ ensureEnabled();
778
+ const Passkeys = await loadPasskeys();
779
+ await client.ensureCsrf();
780
+ const xsrf = readXsrfToken(config.csrf.cookie);
781
+ Passkeys.configure({
782
+ fetch: {
783
+ credentials: "include",
784
+ headers: xsrf ? {
785
+ [config.csrf.header]: xsrf
786
+ } : {}
787
+ }
788
+ });
789
+ return Passkeys;
790
+ }
791
+ return {
792
+ async isSupported () {
793
+ const Passkeys = await loadPasskeys();
794
+ return Passkeys.isSupported();
795
+ },
796
+ async register (name) {
797
+ const Passkeys = await configured();
798
+ return Passkeys.register({
799
+ name,
800
+ routes: {
801
+ options: abs(ep.registerOptions),
802
+ submit: abs(ep.register)
803
+ }
804
+ });
805
+ },
806
+ async login () {
807
+ const Passkeys = await configured();
808
+ await Passkeys.verify({
809
+ routes: {
810
+ options: abs(ep.loginOptions),
811
+ submit: abs(ep.login)
812
+ }
813
+ });
814
+ },
815
+ async confirmPassword () {
816
+ const Passkeys = await configured();
817
+ await Passkeys.verify({
818
+ routes: {
819
+ options: abs(ep.confirmOptions),
820
+ submit: abs(ep.confirm)
821
+ }
822
+ });
823
+ },
824
+ async delete (id) {
825
+ ensureEnabled();
826
+ await client.raw(`${ep.delete}/${encodeURIComponent(id)}`, {
827
+ method: "DELETE"
828
+ });
829
+ }
830
+ };
831
+ }
832
+
833
+ /** In-memory token storage (lost on reload). Default for token mode. */ class MemoryStorage {
834
+ get() {
835
+ return this.token;
836
+ }
837
+ set(token) {
838
+ this.token = token;
839
+ }
840
+ remove() {
841
+ this.token = null;
842
+ }
843
+ constructor(){
844
+ this.token = null;
845
+ }
846
+ }
847
+
848
+ const DEFAULT_KEY = "sanctum.token";
849
+ let warned$1 = false;
850
+ /** Token storage in localStorage. OPT-IN — vulnerable to XSS (see PRD §12). */ class LocalStorage {
851
+ constructor(key = DEFAULT_KEY){
852
+ this.key = key;
853
+ }
854
+ warnOnce() {
855
+ if (warned$1) return;
856
+ warned$1 = true;
857
+ console.warn("[next-sanctum] LocalStorage token storage is vulnerable to XSS. Use it only when necessary (e.g. Capacitor); for the web, prefer HttpOnly cookies + a server proxy.");
858
+ }
859
+ get() {
860
+ if (typeof window === "undefined") return null;
861
+ this.warnOnce();
862
+ return window.localStorage.getItem(this.key);
863
+ }
864
+ set(token) {
865
+ if (typeof window === "undefined") return;
866
+ this.warnOnce();
867
+ window.localStorage.setItem(this.key, token);
868
+ }
869
+ remove() {
870
+ if (typeof window === "undefined") return;
871
+ window.localStorage.removeItem(this.key);
872
+ }
873
+ }
874
+
875
+ let warned = false;
876
+ /**
877
+ * Cookie-based token storage written by the client. NOTE: cookies written by
878
+ * JS CANNOT be HttpOnly. For true HttpOnly, set the cookie via a Route Handler/Server
879
+ * Action and then attach the Bearer on the server (catch-all proxy). This storage is for
880
+ * simple persistence, NOT a replacement for HttpOnly.
881
+ */ class CookieTokenStorage {
882
+ constructor(options = {}){
883
+ this.name = options.name ?? "sanctum_token";
884
+ this.maxAge = options.maxAge ?? 60 * 60 * 24 * 14;
885
+ this.sameSite = options.sameSite ?? "Strict";
886
+ this.secure = options.secure ?? true;
887
+ this.path = options.path ?? "/";
888
+ }
889
+ warnOnce() {
890
+ if (warned) return;
891
+ warned = true;
892
+ console.warn("[next-sanctum] CookieTokenStorage writes a non-HttpOnly cookie that is readable by JS (XSS-exposed). For production web apps, prefer an HttpOnly cookie set server-side + the catch-all proxy.");
893
+ }
894
+ get() {
895
+ const raw = readCookie(this.name);
896
+ if (raw === null || raw === "") return null;
897
+ this.warnOnce();
898
+ try {
899
+ return decodeURIComponent(raw);
900
+ } catch {
901
+ return raw;
902
+ }
903
+ }
904
+ set(token) {
905
+ if (typeof document === "undefined") return;
906
+ this.warnOnce();
907
+ const parts = [
908
+ `${this.name}=${encodeURIComponent(token)}`,
909
+ `Path=${this.path}`,
910
+ `Max-Age=${this.maxAge}`,
911
+ `SameSite=${this.sameSite}`
912
+ ];
913
+ if (this.secure) parts.push("Secure");
914
+ document.cookie = parts.join("; ");
915
+ }
916
+ remove() {
917
+ if (typeof document === "undefined") return;
918
+ document.cookie = `${this.name}=; Path=${this.path}; Max-Age=0`;
919
+ }
920
+ }
921
+
922
+ /** Effective storage: config.storage, or the default MemoryStorage for token mode. */ function resolveTokenStorage(config) {
923
+ if (config.storage) return config.storage;
924
+ if (config.mode === "token") return new MemoryStorage();
925
+ return undefined;
926
+ }
927
+
928
+ const SanctumContext = /*#__PURE__*/ react.createContext(null);
929
+ /** Get the context; throws a clear error if used outside the provider. */ function useSanctumContext() {
930
+ const ctx = react.useContext(SanctumContext);
931
+ if (!ctx) {
932
+ throw new Error("next-sanctum hooks must be used inside <SanctumProvider>.");
933
+ }
934
+ return ctx;
935
+ }
936
+
937
+ function SanctumProvider({ config, initialUser, children }) {
938
+ // Config resolution + client/feature-api creation happens once (on mount).
939
+ const instanceRef = react.useRef(null);
940
+ if (instanceRef.current === null) {
941
+ const resolved = resolveConfig(config);
942
+ const logger = createLogger(resolved.logLevel);
943
+ const emitter = new SanctumEventEmitter();
944
+ emitter.register(resolved.events);
945
+ const storage = resolveTokenStorage(resolved);
946
+ const client = createSanctumClient(resolved, {
947
+ logger,
948
+ emitter,
949
+ getToken: storage ? ()=>storage.get() : undefined
950
+ });
951
+ const auth = createAuthApi(client, resolved, {
952
+ emitter,
953
+ setToken: storage ? (token)=>storage.set(token) : undefined,
954
+ clearToken: storage ? ()=>storage.remove() : undefined
955
+ });
956
+ instanceRef.current = {
957
+ config: resolved,
958
+ client,
959
+ emitter,
960
+ auth,
961
+ registration: createRegistrationApi(client, resolved),
962
+ password: createPasswordApi(client, resolved),
963
+ profile: createProfileApi(client, resolved),
964
+ emailVerification: createEmailVerificationApi(client, resolved),
965
+ twoFactor: createTwoFactorApi(client, resolved),
966
+ passkeys: createPasskeysApi(client, resolved)
967
+ };
968
+ }
969
+ const { config: resolved, client, emitter, auth, registration, password, profile, emailVerification, twoFactor: rawTwoFactor, passkeys: rawPasskeys } = instanceRef.current;
970
+ const [user, setUser] = react.useState(()=>initialUser ?? null);
971
+ const [status, setStatus] = react.useState(()=>initialUser !== undefined ? initialUser ? "authenticated" : "unauthenticated" : resolved.initialRequest ? "loading" : "unauthenticated");
972
+ const userRef = react.useRef(user);
973
+ react.useEffect(()=>{
974
+ userRef.current = user;
975
+ }, [
976
+ user
977
+ ]);
978
+ // De-duplicate concurrent login() calls (double-submit) by sharing the in-flight promise.
979
+ const loginInFlight = react.useRef(null);
980
+ const login = react.useCallback((credentials)=>{
981
+ if (loginInFlight.current) return loginInFlight.current;
982
+ const pending = (async ()=>{
983
+ setStatus("loading");
984
+ try {
985
+ const result = await auth.login(credentials);
986
+ if (result.status === "authenticated") {
987
+ setUser(result.user);
988
+ setStatus("authenticated");
989
+ } else {
990
+ setStatus("unauthenticated");
991
+ }
992
+ return result;
993
+ } catch (error) {
994
+ setStatus(userRef.current ? "authenticated" : "unauthenticated");
995
+ throw error;
996
+ } finally{
997
+ loginInFlight.current = null;
998
+ }
999
+ })();
1000
+ loginInFlight.current = pending;
1001
+ return pending;
1002
+ }, [
1003
+ auth
1004
+ ]);
1005
+ const logout = react.useCallback(async ()=>{
1006
+ try {
1007
+ await auth.logout();
1008
+ } finally{
1009
+ setUser(null);
1010
+ setStatus("unauthenticated");
1011
+ }
1012
+ }, [
1013
+ auth
1014
+ ]);
1015
+ const refresh = react.useCallback(async ()=>{
1016
+ const next = await auth.refreshIdentity();
1017
+ setUser(next);
1018
+ setStatus(next ? "authenticated" : "unauthenticated");
1019
+ return next;
1020
+ }, [
1021
+ auth
1022
+ ]);
1023
+ // Actions that change identity → refresh state after success.
1024
+ const register = react.useCallback(async (payload)=>{
1025
+ await registration.register(payload);
1026
+ await refresh().catch(()=>{});
1027
+ }, [
1028
+ registration,
1029
+ refresh
1030
+ ]);
1031
+ const updateProfile = react.useCallback(async (payload)=>{
1032
+ await profile.updateProfileInformation(payload);
1033
+ await refresh().catch(()=>{});
1034
+ }, [
1035
+ profile,
1036
+ refresh
1037
+ ]);
1038
+ const verifyEmail = react.useCallback(async (payload)=>{
1039
+ await emailVerification.verifyEmail(payload);
1040
+ await refresh().catch(()=>{});
1041
+ }, [
1042
+ emailVerification,
1043
+ refresh
1044
+ ]);
1045
+ const twoFactor = react.useMemo(()=>({
1046
+ ...rawTwoFactor,
1047
+ challenge: async (payload)=>{
1048
+ await rawTwoFactor.challenge(payload);
1049
+ await refresh().catch(()=>{});
1050
+ }
1051
+ }), [
1052
+ rawTwoFactor,
1053
+ refresh
1054
+ ]);
1055
+ const passkeys = react.useMemo(()=>({
1056
+ ...rawPasskeys,
1057
+ login: async ()=>{
1058
+ await rawPasskeys.login();
1059
+ await refresh().catch(()=>{});
1060
+ }
1061
+ }), [
1062
+ rawPasskeys,
1063
+ refresh
1064
+ ]);
1065
+ // Reactive logout on 401 (expired session) + optional redirect.
1066
+ react.useEffect(()=>{
1067
+ const off = emitter.on("error", ({ error })=>{
1068
+ if (error.kind !== "unauthorized") return;
1069
+ // Only react when we currently believe we're authenticated (session expiry) —
1070
+ // not on the initial "am I logged in?" probe 401 for a guest on a public page.
1071
+ if (!userRef.current) return;
1072
+ setUser(null);
1073
+ setStatus("unauthenticated");
1074
+ const target = resolved.redirectIfUnauthenticated;
1075
+ if (target && typeof window !== "undefined") {
1076
+ const safe = safeRedirect(target, "/", {
1077
+ origin: resolved.origin
1078
+ });
1079
+ emitter.emit("redirect", {
1080
+ to: safe,
1081
+ reason: "unauthenticated"
1082
+ });
1083
+ window.location.assign(safe);
1084
+ }
1085
+ });
1086
+ return off;
1087
+ }, [
1088
+ emitter,
1089
+ resolved
1090
+ ]);
1091
+ // Init: emit event + fetch user when not prefetched from the server.
1092
+ react.useEffect(()=>{
1093
+ emitter.emit("init", {
1094
+ user: userRef.current
1095
+ });
1096
+ if (initialUser !== undefined || !resolved.initialRequest) return;
1097
+ let active = true;
1098
+ auth.refreshIdentity().then((next)=>{
1099
+ if (!active) return;
1100
+ setUser(next);
1101
+ setStatus(next ? "authenticated" : "unauthenticated");
1102
+ }).catch(()=>{
1103
+ if (active) setStatus("unauthenticated");
1104
+ });
1105
+ return ()=>{
1106
+ active = false;
1107
+ };
1108
+ }, []);
1109
+ const value = react.useMemo(()=>({
1110
+ config: resolved,
1111
+ client,
1112
+ emitter,
1113
+ user,
1114
+ status,
1115
+ isAuthenticated: status === "authenticated" && user !== null,
1116
+ isLoading: status === "loading",
1117
+ login,
1118
+ logout,
1119
+ refresh,
1120
+ setUser,
1121
+ register,
1122
+ forgotPassword: password.forgotPassword,
1123
+ resetPassword: password.resetPassword,
1124
+ confirmPassword: password.confirmPassword,
1125
+ confirmedPasswordStatus: password.confirmedPasswordStatus,
1126
+ updatePassword: password.updatePassword,
1127
+ updateProfile,
1128
+ resendEmailVerification: emailVerification.resendEmailVerification,
1129
+ verifyEmail,
1130
+ twoFactor,
1131
+ passkeys
1132
+ }), [
1133
+ resolved,
1134
+ client,
1135
+ emitter,
1136
+ user,
1137
+ status,
1138
+ login,
1139
+ logout,
1140
+ refresh,
1141
+ register,
1142
+ updateProfile,
1143
+ verifyEmail,
1144
+ twoFactor,
1145
+ passkeys,
1146
+ password,
1147
+ emailVerification
1148
+ ]);
1149
+ return /*#__PURE__*/ jsxRuntime.jsx(SanctumContext.Provider, {
1150
+ value: value,
1151
+ children: children
1152
+ });
1153
+ }
1154
+
1155
+ /** Authentication & account state and actions (login, register, password, profile, email verification). */ function useAuth() {
1156
+ const ctx = useSanctumContext();
1157
+ return {
1158
+ user: ctx.user,
1159
+ isAuthenticated: ctx.isAuthenticated,
1160
+ isLoading: ctx.isLoading,
1161
+ login: ctx.login,
1162
+ logout: ctx.logout,
1163
+ refresh: ctx.refresh,
1164
+ register: ctx.register,
1165
+ forgotPassword: ctx.forgotPassword,
1166
+ resetPassword: ctx.resetPassword,
1167
+ confirmPassword: ctx.confirmPassword,
1168
+ confirmedPasswordStatus: ctx.confirmedPasswordStatus,
1169
+ updatePassword: ctx.updatePassword,
1170
+ updateProfile: ctx.updateProfile,
1171
+ resendEmailVerification: ctx.resendEmailVerification,
1172
+ verifyEmail: ctx.verifyEmail
1173
+ };
1174
+ }
1175
+
1176
+ /** Reactive user (null when not authenticated). Cast via the generic. */ function useUser() {
1177
+ return useSanctumContext().user;
1178
+ }
1179
+
1180
+ /**
1181
+ * Authenticated fetch on the client. Minimal but sufficient for most cases;
1182
+ * SWR/TanStack Query adapters can be built on top of `useSanctumContext().client`.
1183
+ */ function useApi(path, options = {}) {
1184
+ const { client } = useSanctumContext();
1185
+ const { enabled = true, ...init } = options;
1186
+ const initRef = react.useRef(init);
1187
+ initRef.current = init;
1188
+ // Serialize the request shape so option changes (method/json/body) trigger a refetch,
1189
+ // not just `path`. Header changes are intentionally not part of the key (Headers
1190
+ // aren't reliably serializable) — encode dynamic values into `path`/`json`.
1191
+ const requestKey = JSON.stringify({
1192
+ method: init.method ?? "GET",
1193
+ json: init.json ?? null,
1194
+ body: typeof init.body === "string" ? init.body : null
1195
+ });
1196
+ const [data, setData] = react.useState(undefined);
1197
+ const [error, setError] = react.useState(null);
1198
+ const [isLoading, setIsLoading] = react.useState(enabled);
1199
+ const refetch = react.useCallback(async ()=>{
1200
+ setIsLoading(true);
1201
+ setError(null);
1202
+ try {
1203
+ setData(await client.request(path, initRef.current));
1204
+ } catch (err) {
1205
+ setError(err);
1206
+ } finally{
1207
+ setIsLoading(false);
1208
+ }
1209
+ }, [
1210
+ client,
1211
+ path,
1212
+ requestKey
1213
+ ]);
1214
+ react.useEffect(()=>{
1215
+ if (!enabled) return;
1216
+ let active = true;
1217
+ setIsLoading(true);
1218
+ setError(null);
1219
+ client.request(path, initRef.current).then((result)=>{
1220
+ if (active) setData(result);
1221
+ }).catch((err)=>{
1222
+ if (active) setError(err);
1223
+ }).finally(()=>{
1224
+ if (active) setIsLoading(false);
1225
+ });
1226
+ return ()=>{
1227
+ active = false;
1228
+ };
1229
+ }, [
1230
+ client,
1231
+ path,
1232
+ enabled,
1233
+ requestKey
1234
+ ]);
1235
+ return {
1236
+ data,
1237
+ error,
1238
+ isLoading,
1239
+ refetch
1240
+ };
1241
+ }
1242
+
1243
+ /**
1244
+ * The authenticated HTTP client for imperative requests — i.e. CRUD beyond auth
1245
+ * (create/update/delete or on-demand reads). `client.request<T>(path, { method, json })`
1246
+ * returns parsed JSON; `client.raw(...)` returns the Response. It automatically attaches
1247
+ * CSRF (cookie mode) or Bearer (token mode), the base URL, and credentials.
1248
+ */ function useClient() {
1249
+ return useSanctumContext().client;
1250
+ }
1251
+
1252
+ /**
1253
+ * A typed REST resource over the authenticated client — convenience sugar for CRUD.
1254
+ * Credentials (CSRF/cookie or Bearer) are attached automatically. `TList` defaults to
1255
+ * `T[]`; set it (e.g. `{ data: T[]; meta: … }`) for paginated Laravel resources.
1256
+ *
1257
+ * ```ts
1258
+ * const posts = useResource<Post>("/api/posts")
1259
+ * await posts.list() // GET /api/posts
1260
+ * await posts.create({ title }) // POST /api/posts
1261
+ * await posts.update(1, { title })// PUT /api/posts/1
1262
+ * await posts.delete(1) // DELETE /api/posts/1
1263
+ * ```
1264
+ */ function useResource(basePath) {
1265
+ const { client } = useSanctumContext();
1266
+ return react.useMemo(()=>{
1267
+ const base = basePath.replace(/\/+$/, "");
1268
+ const at = (id)=>`${base}/${encodeURIComponent(String(id))}`;
1269
+ return {
1270
+ list: (init)=>client.request(base, {
1271
+ ...init,
1272
+ method: "GET"
1273
+ }),
1274
+ get: (id, init)=>client.request(at(id), {
1275
+ ...init,
1276
+ method: "GET"
1277
+ }),
1278
+ create: (data, init)=>client.request(base, {
1279
+ ...init,
1280
+ method: "POST",
1281
+ json: data
1282
+ }),
1283
+ update: (id, data, init)=>client.request(at(id), {
1284
+ ...init,
1285
+ method: "PUT",
1286
+ json: data
1287
+ }),
1288
+ patch: (id, data, init)=>client.request(at(id), {
1289
+ ...init,
1290
+ method: "PATCH",
1291
+ json: data
1292
+ }),
1293
+ delete: (id, init)=>client.request(at(id), {
1294
+ ...init,
1295
+ method: "DELETE"
1296
+ })
1297
+ };
1298
+ }, [
1299
+ client,
1300
+ basePath
1301
+ ]);
1302
+ }
1303
+
1304
+ /**
1305
+ * A lightweight mutation hook (Inertia-style lifecycle) for imperative requests —
1306
+ * pair it with `useClient` / `useResource`. Manages `isPending` / `error` / `data`
1307
+ * and fires `onBefore` / `onSuccess` / `onError` / `onFinish`.
1308
+ *
1309
+ * ```tsx
1310
+ * const { request } = useClient()
1311
+ * const create = useMutation(
1312
+ * (vars: { title: string }) => request<Post>("/api/posts", { method: "POST", json: vars }),
1313
+ * { onSuccess: (post) => toast("Created"), onError: (e) => toast(e.message) },
1314
+ * )
1315
+ * <button disabled={create.isPending} onClick={() => create.mutate({ title })}>Save</button>
1316
+ * ```
1317
+ */ function useMutation(mutationFn, options = {}) {
1318
+ const [isPending, setIsPending] = react.useState(false);
1319
+ const [error, setError] = react.useState(null);
1320
+ const [data, setData] = react.useState(undefined);
1321
+ const fnRef = react.useRef(mutationFn);
1322
+ fnRef.current = mutationFn;
1323
+ const optionsRef = react.useRef(options);
1324
+ optionsRef.current = options;
1325
+ const mutateAsync = react.useCallback(async (vars)=>{
1326
+ const opts = optionsRef.current;
1327
+ if (await opts.onBefore?.(vars) === false) {
1328
+ throw new Error("Mutation cancelled in onBefore");
1329
+ }
1330
+ setIsPending(true);
1331
+ setError(null);
1332
+ try {
1333
+ const result = await fnRef.current(vars);
1334
+ setData(result);
1335
+ opts.onSuccess?.(result, vars);
1336
+ return result;
1337
+ } catch (err) {
1338
+ const sanctumError = err;
1339
+ setError(sanctumError);
1340
+ opts.onError?.(sanctumError, vars);
1341
+ throw sanctumError;
1342
+ } finally{
1343
+ setIsPending(false);
1344
+ opts.onFinish?.(vars);
1345
+ }
1346
+ }, []);
1347
+ const mutate = react.useCallback((vars)=>{
1348
+ void mutateAsync(vars).catch(()=>{});
1349
+ }, [
1350
+ mutateAsync
1351
+ ]);
1352
+ const reset = react.useCallback(()=>{
1353
+ setIsPending(false);
1354
+ setError(null);
1355
+ setData(undefined);
1356
+ }, []);
1357
+ return {
1358
+ mutate,
1359
+ mutateAsync,
1360
+ isPending,
1361
+ error,
1362
+ data,
1363
+ reset
1364
+ };
1365
+ }
1366
+
1367
+ /**
1368
+ * Two-factor API (Fortify): challenge during login + management (enable/confirm/disable,
1369
+ * QR, recovery codes). `challenge()` automatically refreshes the identity on success.
1370
+ */ function useTwoFactor() {
1371
+ return useSanctumContext().twoFactor;
1372
+ }
1373
+
1374
+ /**
1375
+ * Passkeys API (interop with @laravel/passkeys). `login()` automatically refreshes
1376
+ * the identity on success. Requires the `@laravel/passkeys` package in the consumer.
1377
+ */ function usePasskeys() {
1378
+ return useSanctumContext().passkeys;
1379
+ }
1380
+
1381
+ exports.ConfigError = ConfigError;
1382
+ exports.CookieTokenStorage = CookieTokenStorage;
1383
+ exports.LocalStorage = LocalStorage;
1384
+ exports.MemoryStorage = MemoryStorage;
1385
+ exports.SanctumError = SanctumError;
1386
+ exports.SanctumProvider = SanctumProvider;
1387
+ exports.ValidationError = ValidationError;
1388
+ exports.useApi = useApi;
1389
+ exports.useAuth = useAuth;
1390
+ exports.useClient = useClient;
1391
+ exports.useMutation = useMutation;
1392
+ exports.usePasskeys = usePasskeys;
1393
+ exports.useResource = useResource;
1394
+ exports.useTwoFactor = useTwoFactor;
1395
+ exports.useUser = useUser;