@rockerone/xprnkit 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -519,6 +519,147 @@ function ExistingComponent() {
519
519
  }
520
520
  ```
521
521
 
522
+ ## Authentication Persistence and SSR
523
+
524
+ ### Overview
525
+
526
+ XPRNKit automatically restores **wallet sessions** via the `restoreSession` config option, but **authentication data** (tokens, permissions, etc.) is **not automatically persisted** to give you full control over your security model.
527
+
528
+ This design is intentional:
529
+ - ✅ **Wallet Connection** = Handled by XPRNKit (via Proton Web SDK)
530
+ - ❌ **Authentication State** = Your responsibility (localStorage, cookies, server sessions, etc.)
531
+
532
+ ### Storage Helper Utilities
533
+
534
+ XPRNKit provides optional helper utilities:
535
+
536
+ ```typescript
537
+ import { authStorage } from 'xprnkit';
538
+
539
+ // Save authentication data
540
+ authStorage.save('user@proton', {
541
+ actor: 'user@proton',
542
+ publicKey: 'PUB_K1_...',
543
+ data: { token: 'jwt-token' },
544
+ expiresAt: Date.now() + 86400000,
545
+ });
546
+
547
+ // Load/clear/list
548
+ const auth = authStorage.load('user@proton');
549
+ authStorage.clear('user@proton');
550
+ authStorage.clearAll();
551
+ const actors = authStorage.listActors();
552
+ ```
553
+
554
+ ### Implementation Patterns
555
+
556
+ #### Pattern 1: Client-Side (localStorage)
557
+
558
+ **Use Case:** Simple apps, prototypes
559
+
560
+ ```typescript
561
+ import { useXPRN, authStorage } from 'xprnkit';
562
+
563
+ function MyApp() {
564
+ const { session, authenticate } = useXPRN();
565
+
566
+ const handleAuth = () => {
567
+ authenticate(
568
+ (authData) => {
569
+ authStorage.save(session.auth.actor.toString(), {
570
+ actor: session.auth.actor.toString(),
571
+ publicKey: session.publicKey.toString(),
572
+ data: authData,
573
+ expiresAt: Date.now() + 86400000,
574
+ });
575
+ },
576
+ (error) => console.error(error)
577
+ );
578
+ };
579
+ }
580
+ ```
581
+
582
+ #### Pattern 2: SSR with Cookies (Next.js)
583
+
584
+ **Use Case:** Next.js apps, production
585
+
586
+ **Client:**
587
+ ```typescript
588
+ 'use client';
589
+ import { useXPRN } from 'xprnkit';
590
+
591
+ function MyApp() {
592
+ const { session, authenticate } = useXPRN();
593
+
594
+ const handleAuth = () => {
595
+ authenticate(
596
+ async (authData) => {
597
+ await fetch('/api/auth/session', {
598
+ method: 'POST',
599
+ body: JSON.stringify({ actor: session.auth.actor, authData }),
600
+ });
601
+ },
602
+ (error) => console.error(error)
603
+ );
604
+ };
605
+ }
606
+ ```
607
+
608
+ **Server:**
609
+ ```typescript
610
+ // app/api/auth/session/route.ts
611
+ import { NextResponse } from 'next/server';
612
+
613
+ export async function POST(request: Request) {
614
+ const body = await request.json();
615
+ const response = NextResponse.json({ success: true });
616
+
617
+ response.cookies.set('xprn_session', createToken(body), {
618
+ httpOnly: true,
619
+ secure: process.env.NODE_ENV === 'production',
620
+ maxAge: 86400,
621
+ });
622
+
623
+ return response;
624
+ }
625
+ ```
626
+
627
+ #### Pattern 3: Server Sessions (Redis/DB)
628
+
629
+ **Use Case:** Scalable production apps
630
+
631
+ ```typescript
632
+ // app/api/session/create/route.ts
633
+ import { redis } from '@/lib/redis';
634
+ import { randomUUID } from 'crypto';
635
+
636
+ export async function POST(request: Request) {
637
+ const body = await request.json();
638
+ const sessionId = randomUUID();
639
+
640
+ await redis.setex(\`session:\${sessionId}\`, 86400, JSON.stringify(body));
641
+
642
+ const response = NextResponse.json({ success: true });
643
+ response.cookies.set('session_id', sessionId, { httpOnly: true });
644
+ return response;
645
+ }
646
+ ```
647
+
648
+ ### Comparison
649
+
650
+ | Approach | Security | SSR | Best For |
651
+ |----------|----------|-----|----------|
652
+ | **localStorage** | Medium | ❌ | Client apps |
653
+ | **Cookies** | High | ✅ | SSR apps |
654
+ | **Server Sessions** | Very High | ✅ | Production |
655
+
656
+ ### Full Examples
657
+
658
+ See the `examples/` directory for complete implementations:
659
+ - `auth-localstorage.example.tsx` - Client-side with localStorage
660
+ - `auth-cookies-ssr.example.tsx` - SSR with httpOnly cookies
661
+ - `auth-server-session.example.tsx` - Server sessions with Redis/PostgreSQL
662
+
522
663
  # Components Documentation
523
664
 
524
665
  # XRPNContainer
package/build/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './providers/XPRNProvider';
2
2
  export * from './components';
3
3
  export * from './utils';
4
+ export * from './services';
4
5
  export type { Link, LinkSession, ProtonWebLink } from "@proton/web-sdk";
5
6
  export type { Api, ApiInterfaces, JsonRpc, JsSignatureProvider } from "@proton/js";
package/build/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './providers/XPRNProvider';
2
2
  export * from './components';
3
3
  export * from './utils';
4
+ export * from './services';
@@ -1,6 +1,7 @@
1
1
  import type { Link, LinkSession, ProtonWebLink } from "@proton/web-sdk";
2
2
  import React from "react";
3
3
  import { JsonRpc } from "@proton/js";
4
+ import { type IdentityProofStatus } from "../services/identity-proof";
4
5
  export type XPRNAuthentication = {
5
6
  actor: string;
6
7
  publicKey: string;
@@ -53,6 +54,8 @@ type XPRNProviderContext = {
53
54
  switchSession: (actor: string) => void;
54
55
  getSessionById: (actor: string) => XPRNSession | null;
55
56
  getSessionByActor: (actor: string) => XPRNSession | null;
57
+ authStatus: IdentityProofStatus;
58
+ getAuthStatus: (actor?: string) => IdentityProofStatus;
56
59
  };
57
60
  export declare const XPRNProvider: React.FunctionComponent<XPRNProviderProps>;
58
61
  export declare function useXPRN(): XPRNProviderContext;
@@ -5,7 +5,7 @@ import ConnectWallet from "@proton/web-sdk";
5
5
  import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef, } from "react";
6
6
  import { JsonRpc } from "@proton/js";
7
7
  import { parseTransactionErrorMessage } from "../utils";
8
- import { proton_wrap } from "../interfaces/proton_wrap";
8
+ import { createIdentityProof, verifyIdentityProof, } from "../services/identity-proof";
9
9
  const XPRNContext = React.createContext({
10
10
  session: null,
11
11
  link: null,
@@ -14,7 +14,7 @@ const XPRNContext = React.createContext({
14
14
  txErrorsStack: null,
15
15
  connect: () => { },
16
16
  disconnect: () => { },
17
- addTransactionError(rawMessage) { },
17
+ addTransactionError() { },
18
18
  authentication: null,
19
19
  authenticate: () => { },
20
20
  getActiveSession: () => null,
@@ -25,6 +25,8 @@ const XPRNContext = React.createContext({
25
25
  switchSession: () => { },
26
26
  getSessionById: () => null,
27
27
  getSessionByActor: () => null,
28
+ authStatus: "idle",
29
+ getAuthStatus: () => "idle",
28
30
  });
29
31
  export const XPRNProvider = ({ children, config, }) => {
30
32
  // Force update mechanism - only triggers re-render when active session changes
@@ -37,19 +39,32 @@ export const XPRNProvider = ({ children, config, }) => {
37
39
  const onSessionRef = useRef();
38
40
  const onProfileRef = useRef();
39
41
  const isRestoringRef = useRef(false);
42
+ // Authentication refs - for duplicate prevention and cancellation
43
+ const authStatusRef = useRef(new Map());
44
+ const authenticatingRef = useRef(new Set());
45
+ const authAbortControllersRef = useRef(new Map());
40
46
  // Derived values - recomputed only when version changes (active session affected)
41
- const { activeSession, session, link, profile, authentication } = useMemo(() => {
47
+ const { activeSession, session, link, profile, authentication, authStatus } = useMemo(() => {
42
48
  const active = activeSessionIdRef.current
43
49
  ? sessionsRef.current.get(activeSessionIdRef.current) ?? null
44
50
  : null;
51
+ const status = activeSessionIdRef.current
52
+ ? authStatusRef.current.get(activeSessionIdRef.current) ?? "idle"
53
+ : "idle";
45
54
  return {
46
55
  activeSession: active,
47
56
  session: active?.session ?? null,
48
57
  link: active?.link ?? null,
49
58
  profile: active?.profile ?? null,
50
59
  authentication: active?.authentication ?? null,
60
+ authStatus: status,
51
61
  };
52
62
  }, [version]);
63
+ // Get auth status for any actor
64
+ const getAuthStatus = useCallback((actor) => {
65
+ const target = actor || activeSessionIdRef.current;
66
+ return target ? authStatusRef.current.get(target) ?? "idle" : "idle";
67
+ }, []);
53
68
  // Session management methods - stable callbacks using refs
54
69
  const getActiveSession = useCallback(() => {
55
70
  return activeSessionIdRef.current
@@ -187,7 +202,7 @@ export const XPRNProvider = ({ children, config, }) => {
187
202
  const addTxError = useCallback((message) => {
188
203
  errorsStackRef.current.push(parseTransactionErrorMessage(message));
189
204
  }, []);
190
- const authenticate = useCallback((success, fail, actor) => {
205
+ const authenticate = useCallback(async (success, fail, actor) => {
191
206
  // Use provided actor or active session
192
207
  const targetActor = actor || activeSessionIdRef.current;
193
208
  if (!targetActor) {
@@ -203,69 +218,77 @@ export const XPRNProvider = ({ children, config, }) => {
203
218
  fail(new Error("Authentication URL not configured"));
204
219
  return;
205
220
  }
206
- const authenticationAction = proton_wrap.generateauth([
207
- {
208
- actor: targetSession.session.auth.actor.toString(),
209
- permission: targetSession.session.auth.permission.toString(),
210
- },
211
- ], {
212
- protonAccount: targetSession.session.auth.actor.toString(),
213
- time: new Date().toISOString().slice(0, -1),
214
- });
221
+ // Prevent duplicate requests
222
+ if (authenticatingRef.current.has(targetActor)) {
223
+ fail(new Error(`Authentication already in progress for ${targetActor}`));
224
+ return;
225
+ }
226
+ // Cancel any previous request for this actor
227
+ authAbortControllersRef.current.get(targetActor)?.abort();
228
+ const abortController = new AbortController();
229
+ authAbortControllersRef.current.set(targetActor, abortController);
230
+ authenticatingRef.current.add(targetActor);
231
+ // Update status
232
+ const updateStatus = (status) => {
233
+ authStatusRef.current.set(targetActor, status);
234
+ if (activeSessionIdRef.current === targetActor) {
235
+ forceUpdate();
236
+ }
237
+ };
215
238
  try {
216
- targetSession.session
217
- .transact({ actions: [authenticationAction] }, { broadcast: false })
218
- .then(res => {
219
- const identityProofBody = {
220
- signer: {
221
- actor: targetSession.session.auth.actor,
222
- permission: targetSession.session.auth.permission,
223
- public_key: targetSession.session.publicKey,
224
- },
225
- transaction: res.resolvedTransaction,
226
- signatures: res.signatures,
227
- };
228
- if (config.authenticationUrl)
229
- fetch(config.authenticationUrl, {
230
- method: "post",
231
- body: JSON.stringify(identityProofBody),
232
- })
233
- .then(res => res.json())
234
- .then(authRes => {
235
- const authData = {
236
- publicKey: targetSession.session.publicKey.toString(),
237
- actor: targetSession.session.auth.actor.toString(),
238
- data: authRes,
239
- };
240
- // Update session with authentication data (mutate ref)
241
- const existingSession = sessionsRef.current.get(targetActor);
242
- if (existingSession) {
243
- sessionsRef.current.set(targetActor, {
244
- ...existingSession,
245
- authentication: authData,
246
- });
247
- }
248
- // Only trigger re-render if this is the active session
249
- if (activeSessionIdRef.current === targetActor) {
250
- forceUpdate();
251
- }
252
- success(authRes);
253
- })
254
- .catch(fail);
255
- })
256
- .catch(fail);
239
+ // Step 1: Sign with wallet
240
+ updateStatus("signing");
241
+ const proof = await createIdentityProof(targetSession.session, {
242
+ signal: abortController.signal,
243
+ });
244
+ // Step 2: Verify with backend
245
+ updateStatus("verifying");
246
+ const authRes = await verifyIdentityProof(proof, { authenticationUrl: config.authenticationUrl }, { signal: abortController.signal });
247
+ // Step 3: Update session with authentication data
248
+ const authData = {
249
+ publicKey: proof.signer.publicKey,
250
+ actor: proof.signer.actor,
251
+ data: authRes,
252
+ };
253
+ const existingSession = sessionsRef.current.get(targetActor);
254
+ if (existingSession) {
255
+ sessionsRef.current.set(targetActor, {
256
+ ...existingSession,
257
+ authentication: authData,
258
+ });
259
+ }
260
+ // Success
261
+ updateStatus("success");
262
+ success(authRes);
257
263
  }
258
- catch (e) {
259
- fail(e);
264
+ catch (error) {
265
+ // Handle abort silently
266
+ if (error instanceof Error && error.name === "AbortError") {
267
+ updateStatus("idle");
268
+ return;
269
+ }
270
+ // Handle other errors
271
+ updateStatus("error");
272
+ fail(error);
260
273
  }
261
- }, [config, forceUpdate]);
274
+ finally {
275
+ authenticatingRef.current.delete(targetActor);
276
+ if (authAbortControllersRef.current.get(targetActor) === abortController) {
277
+ authAbortControllersRef.current.delete(targetActor);
278
+ }
279
+ }
280
+ }, [config.authenticationUrl, forceUpdate]);
262
281
  // Handle authentication for active session
263
282
  useEffect(() => {
264
283
  if (session && activeSessionIdRef.current) {
265
284
  const currentSession = sessionsRef.current.get(activeSessionIdRef.current);
266
285
  if (currentSession && !currentSession.authentication) {
267
286
  if (config.enforceAuthentication && config.authenticationUrl) {
268
- authenticate(() => { }, () => { });
287
+ authenticate(() => {
288
+ console.log(`[XPRNProvider] Auto-authenticated: ${activeSessionIdRef.current}`);
289
+ }, error => {
290
+ console.error("[XPRNProvider] Auto-authentication failed:", error);
291
+ });
269
292
  }
270
293
  }
271
294
  }
@@ -273,7 +296,19 @@ export const XPRNProvider = ({ children, config, }) => {
273
296
  onSessionRef.current(session);
274
297
  onSessionRef.current = undefined; // Reset the ref
275
298
  }
276
- }, [session, authenticate, config.enforceAuthentication, config.authenticationUrl]);
299
+ }, [
300
+ session,
301
+ authenticate,
302
+ config.enforceAuthentication,
303
+ config.authenticationUrl,
304
+ ]);
305
+ // Cleanup abort controllers on unmount
306
+ useEffect(() => {
307
+ return () => {
308
+ authAbortControllersRef.current.forEach(controller => controller.abort());
309
+ authAbortControllersRef.current.clear();
310
+ };
311
+ }, []);
277
312
  // Handle profile callbacks
278
313
  useEffect(() => {
279
314
  if (profile && onProfileRef.current) {
@@ -318,6 +353,9 @@ export const XPRNProvider = ({ children, config, }) => {
318
353
  switchSession,
319
354
  getSessionById,
320
355
  getSessionByActor,
356
+ // Authentication status
357
+ authStatus,
358
+ getAuthStatus,
321
359
  };
322
360
  }, [
323
361
  // Reactive values (change when active session changes)
@@ -325,6 +363,7 @@ export const XPRNProvider = ({ children, config, }) => {
325
363
  link,
326
364
  profile,
327
365
  authentication,
366
+ authStatus,
328
367
  // Stable callbacks (only change when their specific deps change)
329
368
  addTxError,
330
369
  connect,
@@ -338,6 +377,7 @@ export const XPRNProvider = ({ children, config, }) => {
338
377
  switchSession,
339
378
  getSessionById,
340
379
  getSessionByActor,
380
+ getAuthStatus,
341
381
  ]);
342
382
  return (_jsx(XPRNContext.Provider, { value: providerValue, children: children }));
343
383
  };
@@ -0,0 +1,23 @@
1
+ import type { LinkSession } from "@proton/web-sdk";
2
+ import type { IdentityProof } from "./types";
3
+ export type CreateIdentityProofOptions = {
4
+ /** AbortSignal for cancellation */
5
+ signal?: AbortSignal;
6
+ };
7
+ /**
8
+ * Creates an identity proof by signing a generateauth action with the wallet.
9
+ *
10
+ * This is a pure function that handles only the wallet signing step.
11
+ * It does NOT send the proof to any backend - use `verifyIdentityProof` for that.
12
+ *
13
+ * @param session - The LinkSession from the connected wallet
14
+ * @param options - Optional configuration (abort signal)
15
+ * @returns Promise resolving to the signed IdentityProof
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const proof = await createIdentityProof(session);
20
+ * // proof contains: { signer, transaction, signatures }
21
+ * ```
22
+ */
23
+ export declare function createIdentityProof(session: LinkSession, options?: CreateIdentityProofOptions): Promise<IdentityProof>;
@@ -0,0 +1,47 @@
1
+ import { proton_wrap } from "../../interfaces/proton_wrap";
2
+ /**
3
+ * Creates an identity proof by signing a generateauth action with the wallet.
4
+ *
5
+ * This is a pure function that handles only the wallet signing step.
6
+ * It does NOT send the proof to any backend - use `verifyIdentityProof` for that.
7
+ *
8
+ * @param session - The LinkSession from the connected wallet
9
+ * @param options - Optional configuration (abort signal)
10
+ * @returns Promise resolving to the signed IdentityProof
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const proof = await createIdentityProof(session);
15
+ * // proof contains: { signer, transaction, signatures }
16
+ * ```
17
+ */
18
+ export async function createIdentityProof(session, options) {
19
+ // Check for abort before starting
20
+ if (options?.signal?.aborted) {
21
+ throw new DOMException("Aborted", "AbortError");
22
+ }
23
+ const actor = session.auth.actor.toString();
24
+ const permission = session.auth.permission.toString();
25
+ // Generate the authentication action
26
+ const authenticationAction = proton_wrap.generateauth([{ actor, permission }], {
27
+ protonAccount: actor,
28
+ time: new Date().toISOString().slice(0, -1),
29
+ });
30
+ // Sign the transaction without broadcasting
31
+ const txResult = await session.transact({ actions: [authenticationAction] }, { broadcast: false });
32
+ // Check for abort after wallet interaction
33
+ if (options?.signal?.aborted) {
34
+ throw new DOMException("Aborted", "AbortError");
35
+ }
36
+ // Build the identity proof
37
+ const proof = {
38
+ signer: {
39
+ actor,
40
+ permission,
41
+ publicKey: session.publicKey.toString(),
42
+ },
43
+ transaction: txResult.resolvedTransaction,
44
+ signatures: txResult.signatures.map(sig => sig.toString()),
45
+ };
46
+ return proof;
47
+ }
@@ -0,0 +1,6 @@
1
+ export type { IdentityProof, IdentityProofConfig, IdentityProofResult, IdentityProofSigner, IdentityProofStatus, UseIdentityProofOptions, UseIdentityProofReturn, } from "./types";
2
+ export { createIdentityProof } from "./create-identity-proof";
3
+ export type { CreateIdentityProofOptions } from "./create-identity-proof";
4
+ export { verifyIdentityProof } from "./verify-identity-proof";
5
+ export type { VerifyIdentityProofOptions } from "./verify-identity-proof";
6
+ export { useIdentityProof } from "./use-identity-proof";
@@ -0,0 +1,5 @@
1
+ // Pure functions
2
+ export { createIdentityProof } from "./create-identity-proof";
3
+ export { verifyIdentityProof } from "./verify-identity-proof";
4
+ // React hook
5
+ export { useIdentityProof } from "./use-identity-proof";
@@ -0,0 +1,62 @@
1
+ import type { LinkSession } from "@proton/web-sdk";
2
+ /**
3
+ * Signer information for identity proof
4
+ */
5
+ export type IdentityProofSigner = {
6
+ actor: string;
7
+ permission: string;
8
+ publicKey: string;
9
+ };
10
+ /**
11
+ * Identity proof generated from wallet signing
12
+ */
13
+ export type IdentityProof = {
14
+ signer: IdentityProofSigner;
15
+ transaction: any;
16
+ signatures: string[];
17
+ };
18
+ /**
19
+ * Result of identity proof verification
20
+ */
21
+ export type IdentityProofResult<T = any> = {
22
+ proof: IdentityProof;
23
+ response: T;
24
+ };
25
+ /**
26
+ * Configuration for identity proof verification
27
+ */
28
+ export type IdentityProofConfig = {
29
+ authenticationUrl: string;
30
+ headers?: Record<string, string>;
31
+ timeout?: number;
32
+ };
33
+ /**
34
+ * Status of identity proof process
35
+ */
36
+ export type IdentityProofStatus = "idle" | "signing" | "verifying" | "success" | "error";
37
+ /**
38
+ * Options for useIdentityProof hook
39
+ */
40
+ export type UseIdentityProofOptions = {
41
+ session: LinkSession | null;
42
+ config?: IdentityProofConfig;
43
+ onSuccess?: (result: IdentityProofResult) => void;
44
+ onError?: (error: Error) => void;
45
+ };
46
+ /**
47
+ * Return type for useIdentityProof hook
48
+ */
49
+ export type UseIdentityProofReturn = {
50
+ /** Trigger authentication flow */
51
+ authenticate: () => Promise<IdentityProofResult | null>;
52
+ /** Current status of the authentication process */
53
+ status: IdentityProofStatus;
54
+ /** Error if authentication failed */
55
+ error: Error | null;
56
+ /** Result if authentication succeeded */
57
+ result: IdentityProofResult | null;
58
+ /** Reset state to idle */
59
+ reset: () => void;
60
+ /** Convenience boolean for loading states */
61
+ isAuthenticating: boolean;
62
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import type { UseIdentityProofOptions, UseIdentityProofReturn } from "./types";
2
+ /**
3
+ * React hook for identity proof authentication.
4
+ *
5
+ * This is the primary API for authentication in XPRNKit.
6
+ * It combines wallet signing and backend verification into a single flow
7
+ * with state management, duplicate prevention, and cleanup handling.
8
+ *
9
+ * @param options - Configuration options
10
+ * @returns Object with authenticate function and state
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * function AuthButton() {
15
+ * const { session } = useXPRN();
16
+ * const {
17
+ * authenticate,
18
+ * status,
19
+ * error,
20
+ * isAuthenticating
21
+ * } = useIdentityProof({
22
+ * session,
23
+ * config: { authenticationUrl: '/api/auth' },
24
+ * onSuccess: (result) => console.log('Authenticated!', result),
25
+ * });
26
+ *
27
+ * return (
28
+ * <button onClick={authenticate} disabled={isAuthenticating}>
29
+ * {status === 'signing' && 'Sign in wallet...'}
30
+ * {status === 'verifying' && 'Verifying...'}
31
+ * {status === 'idle' && 'Authenticate'}
32
+ * {status === 'error' && 'Retry'}
33
+ * </button>
34
+ * );
35
+ * }
36
+ * ```
37
+ */
38
+ export declare function useIdentityProof(options: UseIdentityProofOptions): UseIdentityProofReturn;
@@ -0,0 +1,143 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { createIdentityProof } from "./create-identity-proof";
4
+ import { verifyIdentityProof } from "./verify-identity-proof";
5
+ /**
6
+ * React hook for identity proof authentication.
7
+ *
8
+ * This is the primary API for authentication in XPRNKit.
9
+ * It combines wallet signing and backend verification into a single flow
10
+ * with state management, duplicate prevention, and cleanup handling.
11
+ *
12
+ * @param options - Configuration options
13
+ * @returns Object with authenticate function and state
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * function AuthButton() {
18
+ * const { session } = useXPRN();
19
+ * const {
20
+ * authenticate,
21
+ * status,
22
+ * error,
23
+ * isAuthenticating
24
+ * } = useIdentityProof({
25
+ * session,
26
+ * config: { authenticationUrl: '/api/auth' },
27
+ * onSuccess: (result) => console.log('Authenticated!', result),
28
+ * });
29
+ *
30
+ * return (
31
+ * <button onClick={authenticate} disabled={isAuthenticating}>
32
+ * {status === 'signing' && 'Sign in wallet...'}
33
+ * {status === 'verifying' && 'Verifying...'}
34
+ * {status === 'idle' && 'Authenticate'}
35
+ * {status === 'error' && 'Retry'}
36
+ * </button>
37
+ * );
38
+ * }
39
+ * ```
40
+ */
41
+ export function useIdentityProof(options) {
42
+ const { session, config, onSuccess, onError } = options;
43
+ // State
44
+ const [status, setStatus] = useState("idle");
45
+ const [error, setError] = useState(null);
46
+ const [result, setResult] = useState(null);
47
+ // Refs for cleanup and duplicate prevention
48
+ const abortControllerRef = useRef(null);
49
+ const isAuthenticatingRef = useRef(false);
50
+ // Cleanup on unmount
51
+ useEffect(() => {
52
+ return () => {
53
+ abortControllerRef.current?.abort();
54
+ };
55
+ }, []);
56
+ // Reset function
57
+ const reset = useCallback(() => {
58
+ abortControllerRef.current?.abort();
59
+ abortControllerRef.current = null;
60
+ isAuthenticatingRef.current = false;
61
+ setStatus("idle");
62
+ setError(null);
63
+ setResult(null);
64
+ }, []);
65
+ // Main authenticate function
66
+ const authenticate = useCallback(async () => {
67
+ // Validate inputs
68
+ if (!session) {
69
+ const err = new Error("No session available for authentication");
70
+ setError(err);
71
+ setStatus("error");
72
+ onError?.(err);
73
+ return null;
74
+ }
75
+ if (!config?.authenticationUrl) {
76
+ const err = new Error("Authentication URL not configured");
77
+ setError(err);
78
+ setStatus("error");
79
+ onError?.(err);
80
+ return null;
81
+ }
82
+ // Prevent duplicate requests
83
+ if (isAuthenticatingRef.current) {
84
+ const err = new Error("Authentication already in progress");
85
+ onError?.(err);
86
+ return null;
87
+ }
88
+ // Cancel any previous request
89
+ abortControllerRef.current?.abort();
90
+ const abortController = new AbortController();
91
+ abortControllerRef.current = abortController;
92
+ isAuthenticatingRef.current = true;
93
+ try {
94
+ // Step 1: Sign with wallet
95
+ setStatus("signing");
96
+ setError(null);
97
+ const proof = await createIdentityProof(session, {
98
+ signal: abortController.signal,
99
+ });
100
+ // Step 2: Verify with backend
101
+ setStatus("verifying");
102
+ const response = await verifyIdentityProof(proof, config, {
103
+ signal: abortController.signal,
104
+ });
105
+ // Success
106
+ const identityResult = {
107
+ proof,
108
+ response,
109
+ };
110
+ setResult(identityResult);
111
+ setStatus("success");
112
+ onSuccess?.(identityResult);
113
+ return identityResult;
114
+ }
115
+ catch (err) {
116
+ // Handle abort silently
117
+ if (err instanceof Error && err.name === "AbortError") {
118
+ setStatus("idle");
119
+ return null;
120
+ }
121
+ // Handle other errors
122
+ const error = err instanceof Error ? err : new Error(String(err));
123
+ setError(error);
124
+ setStatus("error");
125
+ onError?.(error);
126
+ return null;
127
+ }
128
+ finally {
129
+ isAuthenticatingRef.current = false;
130
+ if (abortControllerRef.current === abortController) {
131
+ abortControllerRef.current = null;
132
+ }
133
+ }
134
+ }, [session, config, onSuccess, onError]);
135
+ return {
136
+ authenticate,
137
+ status,
138
+ error,
139
+ result,
140
+ reset,
141
+ isAuthenticating: status === "signing" || status === "verifying",
142
+ };
143
+ }
@@ -0,0 +1,25 @@
1
+ import type { IdentityProof, IdentityProofConfig } from "./types";
2
+ export type VerifyIdentityProofOptions = {
3
+ /** AbortSignal for cancellation */
4
+ signal?: AbortSignal;
5
+ };
6
+ /**
7
+ * Verifies an identity proof by sending it to the authentication backend.
8
+ *
9
+ * This is a pure function that handles only the backend verification step.
10
+ * Use `createIdentityProof` first to generate the proof.
11
+ *
12
+ * @param proof - The identity proof from createIdentityProof
13
+ * @param config - Configuration with authentication URL
14
+ * @param options - Optional configuration (abort signal)
15
+ * @returns Promise resolving to the backend response
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const proof = await createIdentityProof(session);
20
+ * const response = await verifyIdentityProof(proof, {
21
+ * authenticationUrl: '/api/auth/verify'
22
+ * });
23
+ * ```
24
+ */
25
+ export declare function verifyIdentityProof<T = any>(proof: IdentityProof, config: IdentityProofConfig, options?: VerifyIdentityProofOptions): Promise<T>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Verifies an identity proof by sending it to the authentication backend.
3
+ *
4
+ * This is a pure function that handles only the backend verification step.
5
+ * Use `createIdentityProof` first to generate the proof.
6
+ *
7
+ * @param proof - The identity proof from createIdentityProof
8
+ * @param config - Configuration with authentication URL
9
+ * @param options - Optional configuration (abort signal)
10
+ * @returns Promise resolving to the backend response
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const proof = await createIdentityProof(session);
15
+ * const response = await verifyIdentityProof(proof, {
16
+ * authenticationUrl: '/api/auth/verify'
17
+ * });
18
+ * ```
19
+ */
20
+ export async function verifyIdentityProof(proof, config, options) {
21
+ // Check for abort before starting
22
+ if (options?.signal?.aborted) {
23
+ throw new DOMException("Aborted", "AbortError");
24
+ }
25
+ // Build request body
26
+ const requestBody = {
27
+ signer: {
28
+ actor: proof.signer.actor,
29
+ permission: proof.signer.permission,
30
+ public_key: proof.signer.publicKey,
31
+ },
32
+ transaction: proof.transaction,
33
+ signatures: proof.signatures,
34
+ };
35
+ // Build fetch options
36
+ const fetchOptions = {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/json",
40
+ ...config.headers,
41
+ },
42
+ body: JSON.stringify(requestBody),
43
+ signal: options?.signal,
44
+ };
45
+ // Make the request
46
+ const response = await fetch(config.authenticationUrl, fetchOptions);
47
+ // Check for abort after fetch
48
+ if (options?.signal?.aborted) {
49
+ throw new DOMException("Aborted", "AbortError");
50
+ }
51
+ // Handle non-OK responses
52
+ if (!response.ok) {
53
+ const errorText = await response.text().catch(() => "Unknown error");
54
+ throw new Error(`Authentication failed: ${response.status} ${response.statusText} - ${errorText}`);
55
+ }
56
+ // Parse and return response
57
+ const result = await response.json();
58
+ return result;
59
+ }
@@ -0,0 +1 @@
1
+ export * from "./identity-proof";
@@ -0,0 +1,2 @@
1
+ // Identity Proof Service
2
+ export * from "./identity-proof";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rockerone/xprnkit",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "source": "src/index.ts",
5
5
  "main": "build/index.js",
6
6
  "type": "module",