@rockerone/xprnkit 0.3.9 → 0.3.11

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.
@@ -5,7 +5,7 @@ type XPRNIdentityProofGateProps = {
5
5
  */
6
6
  children: React.ReactNode;
7
7
  /**
8
- * Content to render when identity proof is required but not yet obtained.
8
+ * Content to render when identity proof is not obtained.
9
9
  * If not provided, renders nothing.
10
10
  */
11
11
  fallback?: React.ReactNode;
@@ -24,11 +24,6 @@ type XPRNIdentityProofGateProps = {
24
24
  * If not provided, uses fallback.
25
25
  */
26
26
  notConnected?: React.ReactNode;
27
- /**
28
- * If true, only gates when identityProof.required is true in config.
29
- * If false (default), gates whenever identity proof is configured.
30
- */
31
- onlyWhenRequired?: boolean;
32
27
  };
33
28
  /**
34
29
  * Conditionally renders children based on identity proof status.
@@ -51,9 +46,7 @@ export declare const XPRNIdentityProofGate: React.FunctionComponent<XPRNIdentity
51
46
  * Hook to check identity proof gate status
52
47
  * Returns the same information used by XPRNIdentityProofGate
53
48
  */
54
- export declare function useIdentityProofGate(options?: {
55
- onlyWhenRequired?: boolean;
56
- }): {
49
+ export declare function useIdentityProofGate(): {
57
50
  isGateActive: boolean;
58
51
  isConnected: boolean;
59
52
  isInProgress: boolean;
@@ -17,16 +17,12 @@ import { useXPRN } from "../../providers/XPRNProvider";
17
17
  * </XPRNIdentityProofGate>
18
18
  * ```
19
19
  */
20
- export const XPRNIdentityProofGate = ({ children, fallback = null, loading, error, notConnected, onlyWhenRequired = false, }) => {
21
- const { session, identityProof, identityProofStatus, isIdentityProofEnabled, isIdentityProofRequired, } = useXPRN();
20
+ export const XPRNIdentityProofGate = ({ children, fallback = null, loading, error, notConnected, }) => {
21
+ const { session, identityProof, identityProofStatus, isIdentityProofEnabled, } = useXPRN();
22
22
  // If identity proof is not enabled, render children directly
23
23
  if (!isIdentityProofEnabled) {
24
24
  return _jsx(_Fragment, { children: children });
25
25
  }
26
- // If onlyWhenRequired is true and identity proof is not required, render children
27
- if (onlyWhenRequired && !isIdentityProofRequired) {
28
- return _jsx(_Fragment, { children: children });
29
- }
30
26
  // No session connected
31
27
  if (!session) {
32
28
  return _jsx(_Fragment, { children: notConnected ?? fallback });
@@ -51,26 +47,25 @@ XPRNIdentityProofGate.displayName = "XPRNIdentityProofGate";
51
47
  * Hook to check identity proof gate status
52
48
  * Returns the same information used by XPRNIdentityProofGate
53
49
  */
54
- export function useIdentityProofGate(options) {
55
- const { session, identityProof, identityProofStatus, isIdentityProofEnabled, isIdentityProofRequired, needsIdentityProof, requestIdentityProof, } = useXPRN();
56
- const onlyWhenRequired = options?.onlyWhenRequired ?? false;
57
- // Determine if gating should be active
58
- const isGateActive = isIdentityProofEnabled && (!onlyWhenRequired || isIdentityProofRequired);
50
+ export function useIdentityProofGate() {
51
+ const { session, identityProof, identityProofStatus, isIdentityProofEnabled, requestIdentityProof, } = useXPRN();
59
52
  // Determine current state
60
53
  const isConnected = session !== null;
61
54
  const isInProgress = identityProofStatus === "signing" || identityProofStatus === "verifying" || identityProofStatus === "validating";
62
55
  const hasError = identityProofStatus === "error" || identityProofStatus === "expired";
63
56
  const isVerified = identityProof !== null;
64
57
  // Should render protected content?
65
- const shouldRenderChildren = !isGateActive || isVerified;
58
+ const shouldRenderChildren = !isIdentityProofEnabled || isVerified;
59
+ // Needs identity proof: enabled, connected, and not yet verified
60
+ const needsIdentityProof = isIdentityProofEnabled && isConnected && !isVerified;
66
61
  return {
67
- isGateActive,
62
+ isGateActive: isIdentityProofEnabled,
68
63
  isConnected,
69
64
  isInProgress,
70
65
  hasError,
71
66
  isVerified,
72
67
  shouldRenderChildren,
73
- needsIdentityProof: isGateActive && needsIdentityProof,
68
+ needsIdentityProof,
74
69
  requestIdentityProof,
75
70
  identityProofStatus,
76
71
  };
@@ -7,7 +7,7 @@ import { useXPRN } from "../../providers/XPRNProvider";
7
7
  * Get badge styling based on identity proof status
8
8
  */
9
9
  function useIdentityProofBadgeStyle() {
10
- const { identityProofStatus, isIdentityProofEnabled, needsIdentityProof } = useXPRN();
10
+ const { identityProofStatus, isIdentityProofEnabled, identityProof, session } = useXPRN();
11
11
  return useMemo(() => {
12
12
  if (!isIdentityProofEnabled) {
13
13
  return { visible: false, className: "" };
@@ -18,6 +18,7 @@ function useIdentityProofBadgeStyle() {
18
18
  const isSuccess = identityProofStatus === "success";
19
19
  const isError = identityProofStatus === "error" || identityProofStatus === "expired";
20
20
  const isIdle = identityProofStatus === "idle";
21
+ const needsIdentityProof = session !== null && identityProof === null;
21
22
  // Build className based on status
22
23
  let className = "";
23
24
  if (isPending) {
@@ -42,12 +43,12 @@ function useIdentityProofBadgeStyle() {
42
43
  isSuccess,
43
44
  isError,
44
45
  };
45
- }, [identityProofStatus, isIdentityProofEnabled, needsIdentityProof]);
46
+ }, [identityProofStatus, isIdentityProofEnabled, identityProof, session]);
46
47
  }
47
- export const XPRNSessionName = ({ children, className, }) => {
48
- const { profile, session, config } = useXPRN();
48
+ export const XPRNSessionName = ({ children, className, showIdentityProof = false, }) => {
49
+ const { profile, session } = useXPRN();
49
50
  const badgeStyle = useIdentityProofBadgeStyle();
50
- const showBadge = (config?.identityProof?.required || config?.identityProof?.enforceOnConnect) && badgeStyle.visible;
51
+ const showBadge = showIdentityProof && badgeStyle.visible;
51
52
  const rootClasses = classNames({
52
53
  "flex gap-2 items-center justify-center": true,
53
54
  [`${className}`]: className,
@@ -6,15 +6,18 @@ import { type StoredSessionRef } from "../utils";
6
6
  import { type IdentityProofStatus } from "../services/identity-proof";
7
7
  /**
8
8
  * Identity proof data returned from authentication
9
+ * All fields are optional - dev can store whatever they want
9
10
  */
10
11
  export type XPRNIdentityProof = {
11
- actor: string;
12
+ actor?: string;
12
13
  /** Public key (optional - not available for browser/WebAuth wallets) */
13
14
  publicKey?: string;
14
15
  /** JWT token from the identity proof verification */
15
16
  token?: string;
16
17
  /** Additional data from the verification response */
17
18
  data?: any;
19
+ /** Allow any additional fields for custom flows */
20
+ [key: string]: any;
18
21
  };
19
22
  /**
20
23
  * @deprecated Use XPRNIdentityProof instead
@@ -35,12 +38,8 @@ export type XPRNIdentityProofConfig = {
35
38
  validationUrl?: string;
36
39
  /** Time in seconds before expiration to trigger validation (default: 300) */
37
40
  validationBuffer?: number;
38
- /** Automatically authenticate on connect (default: false) */
39
- enforceOnConnect?: boolean;
40
41
  /** Automatically re-authenticate when token expires (default: false) */
41
42
  autoReauthenticate?: boolean;
42
- /** Require identity proof for the app (default: false) */
43
- required?: boolean;
44
43
  /** Additional headers for identity proof requests */
45
44
  headers?: Record<string, string>;
46
45
  /** Request timeout in milliseconds */
@@ -58,8 +57,6 @@ export type XPRProviderConfig = {
58
57
  identityProof?: XPRNIdentityProofConfig;
59
58
  /** @deprecated Use identityProof.createUrl instead */
60
59
  authenticationUrl?: string;
61
- /** @deprecated Use identityProof.enforceOnConnect instead */
62
- enforceAuthentication?: boolean;
63
60
  };
64
61
  type XPRNProviderProps = {
65
62
  children: React.ReactNode | React.ReactNode[];
@@ -73,15 +70,17 @@ type XPRNProviderContext = {
73
70
  rpc: JsonRpc | null;
74
71
  identityProof: XPRNIdentityProof | null;
75
72
  identityProofStatus: IdentityProofStatus;
76
- /** True if identity proof is configured but not yet obtained for current session */
77
- needsIdentityProof: boolean;
78
- /** True if identity proof is configured with required=true */
79
- isIdentityProofRequired: boolean;
80
73
  /** True if identity proof is enabled (config.identityProof is defined) */
81
74
  isIdentityProofEnabled: boolean;
82
75
  connect: (restore?: boolean, onSession?: (session: LinkSession, link: ProtonWebLink | Link) => void, onProfile?: (profile: XPRNProfile) => void, onIdentityProof?: (proof: XPRNIdentityProof) => void, onIdentityProofError?: (error: Error) => void) => void;
83
76
  disconnect: () => Promise<void>;
84
77
  requestIdentityProof: (success: (res: XPRNIdentityProof) => void, fail: (e: any) => void) => void;
78
+ /** Set identity proof data manually (for custom auth flows) */
79
+ setIdentityProof: (data: XPRNIdentityProof | null) => void;
80
+ /** Clear identity proof and reset status to idle */
81
+ clearIdentityProof: () => void;
82
+ /** Set identity proof status manually (for custom auth flows) */
83
+ setIdentityProofStatus: (status: IdentityProofStatus) => void;
85
84
  listStoredSessions: () => StoredSessionRef[];
86
85
  switchToSession: (auth: string, chainId: string) => Promise<void>;
87
86
  addTransactionError: (rawMessage: string) => void;
@@ -14,12 +14,13 @@ const XPRNContext = React.createContext({
14
14
  rpc: null,
15
15
  identityProof: null,
16
16
  identityProofStatus: "idle",
17
- needsIdentityProof: false,
18
- isIdentityProofRequired: false,
19
17
  isIdentityProofEnabled: false,
20
18
  connect: () => { },
21
19
  disconnect: async () => { },
22
20
  requestIdentityProof: () => { },
21
+ setIdentityProof: () => { },
22
+ clearIdentityProof: () => { },
23
+ setIdentityProofStatus: () => { },
23
24
  listStoredSessions: () => [],
24
25
  switchToSession: async () => { },
25
26
  addTransactionError: () => { },
@@ -44,15 +45,12 @@ export const XPRNProvider = ({ children, config, }) => {
44
45
  if (config.authenticationUrl) {
45
46
  return {
46
47
  createUrl: config.authenticationUrl,
47
- enforceOnConnect: config.enforceAuthentication,
48
48
  };
49
49
  }
50
50
  return null;
51
- }, [config.identityProof, config.authenticationUrl, config.enforceAuthentication]);
52
- // Helper booleans
51
+ }, [config.identityProof, config.authenticationUrl]);
52
+ // Helper boolean
53
53
  const isIdentityProofEnabled = identityProofConfig !== null;
54
- const isIdentityProofRequired = identityProofConfig?.required ?? false;
55
- const needsIdentityProof = isIdentityProofEnabled && session !== null && identityProof === null;
56
54
  // Internal refs
57
55
  const jsonRpcRef = useRef(new JsonRpc(config.endpoints));
58
56
  const errorsStackRef = useRef([]);
@@ -64,8 +62,12 @@ export const XPRNProvider = ({ children, config, }) => {
64
62
  const isAuthenticatingRef = useRef(false);
65
63
  const isSwitchingSessionRef = useRef(false);
66
64
  const sessionRef = useRef(null);
67
- // Keep sessionRef in sync with session state
65
+ const linkRef = useRef(null);
66
+ // Queue for pending callbacks when requestIdentityProof is called while already in progress
67
+ const pendingCallbacksRef = useRef([]);
68
+ // Keep refs in sync with state
68
69
  sessionRef.current = session;
70
+ linkRef.current = link;
69
71
  // List stored sessions from proton-web-sdk localStorage
70
72
  const listStoredSessions = useCallback(() => {
71
73
  return sessionStorage.getLinkList(config.requesterAccount);
@@ -222,12 +224,15 @@ export const XPRNProvider = ({ children, config, }) => {
222
224
  });
223
225
  }, [config]);
224
226
  const disconnect = useCallback(async () => {
225
- if (!session || !link) {
227
+ // Use refs to get latest values (avoid stale closure issues)
228
+ const currentSession = sessionRef.current;
229
+ const currentLink = linkRef.current;
230
+ if (!currentSession || !currentLink) {
226
231
  console.warn("No session to disconnect");
227
232
  return;
228
233
  }
229
234
  try {
230
- await link.removeSession(config.requesterAccount, session.auth, session.chainId);
235
+ await currentLink.removeSession(config.requesterAccount, currentSession.auth, currentSession.chainId);
231
236
  }
232
237
  catch (e) {
233
238
  console.error("Error removing session:", e);
@@ -238,7 +243,7 @@ export const XPRNProvider = ({ children, config, }) => {
238
243
  setIdentityProof(null);
239
244
  setIdentityProofStatus("idle");
240
245
  // Note: Keep link reference for potential session switching
241
- }, [session, link, config.requesterAccount]);
246
+ }, [config.requesterAccount]);
242
247
  const addTxError = useCallback((message) => {
243
248
  errorsStackRef.current.push(parseTransactionErrorMessage(message));
244
249
  }, []);
@@ -253,8 +258,9 @@ export const XPRNProvider = ({ children, config, }) => {
253
258
  fail(new Error("Identity proof not configured"));
254
259
  return;
255
260
  }
261
+ // Idempotent: if already in progress, queue callbacks to be called when complete
256
262
  if (isAuthenticatingRef.current) {
257
- fail(new Error("Identity proof already in progress"));
263
+ pendingCallbacksRef.current.push({ success, fail });
258
264
  return;
259
265
  }
260
266
  isAuthenticatingRef.current = true;
@@ -286,6 +292,12 @@ export const XPRNProvider = ({ children, config, }) => {
286
292
  if (onIdentityProofRef.current) {
287
293
  onIdentityProofRef.current(identityProofData);
288
294
  }
295
+ // Call all queued callbacks with success
296
+ const pendingCallbacks = pendingCallbacksRef.current;
297
+ pendingCallbacksRef.current = [];
298
+ for (const cb of pendingCallbacks) {
299
+ cb.success(identityProofData);
300
+ }
289
301
  }
290
302
  catch (error) {
291
303
  setIdentityProofStatus("error");
@@ -294,6 +306,12 @@ export const XPRNProvider = ({ children, config, }) => {
294
306
  if (onIdentityProofErrorRef.current) {
295
307
  onIdentityProofErrorRef.current(error instanceof Error ? error : new Error(String(error)));
296
308
  }
309
+ // Call all queued callbacks with error
310
+ const pendingCallbacks = pendingCallbacksRef.current;
311
+ pendingCallbacksRef.current = [];
312
+ for (const cb of pendingCallbacks) {
313
+ cb.fail(error);
314
+ }
297
315
  }
298
316
  finally {
299
317
  isAuthenticatingRef.current = false;
@@ -359,7 +377,7 @@ export const XPRNProvider = ({ children, config, }) => {
359
377
  return false;
360
378
  }
361
379
  }, [identityProofConfig, config.requesterAccount]);
362
- // Handle token restoration and auto-authentication when session is established
380
+ // Handle token restoration when session is established (no auto-authentication)
363
381
  // Note: This effect is skipped when switchToSession is handling the identity proof state
364
382
  useEffect(() => {
365
383
  // Skip if we're in the middle of a session switch (switchToSession handles this)
@@ -367,28 +385,19 @@ export const XPRNProvider = ({ children, config, }) => {
367
385
  return;
368
386
  if (!session || identityProof)
369
387
  return;
370
- if (!identityProofConfig)
388
+ if (!identityProofConfig?.validationUrl)
371
389
  return;
372
390
  const actor = session.auth.actor.toString();
373
391
  const permission = session.auth.permission.toString();
374
392
  const chainId = session.chainId.toString();
375
- // Check if there's a stored token and try to validate it
393
+ // Check if there's a stored token and try to validate it silently
376
394
  const storedEntry = sessionStorage.get(config.requesterAccount, { actor, permission }, chainId);
377
395
  const hasStoredToken = storedEntry?.identityProofToken && typeof storedEntry.identityProofToken === 'string' && storedEntry.identityProofToken.length > 0;
378
- if (hasStoredToken && identityProofConfig.validationUrl) {
379
- // Try to validate stored token silently
380
- validateStoredToken(actor, permission, chainId).then(isValid => {
381
- if (!isValid && (identityProofConfig.enforceOnConnect || identityProofConfig.required)) {
382
- // Token invalid and identity proof is required - trigger full auth flow
383
- requestIdentityProof(() => { }, () => { });
384
- }
385
- });
386
- }
387
- else if (identityProofConfig.enforceOnConnect || identityProofConfig.required) {
388
- // No stored token (or no validation URL) but identity proof is required - trigger auth flow
389
- requestIdentityProof(() => { }, () => { });
396
+ if (hasStoredToken) {
397
+ // Try to validate stored token silently - dev handles the result via callbacks
398
+ validateStoredToken(actor, permission, chainId);
390
399
  }
391
- }, [session, identityProof, identityProofConfig, config.requesterAccount, validateStoredToken, requestIdentityProof]);
400
+ }, [session, identityProof, identityProofConfig, config.requesterAccount, validateStoredToken]);
392
401
  // Handle profile callbacks
393
402
  useEffect(() => {
394
403
  if (profile && onProfileRef.current) {
@@ -405,6 +414,11 @@ export const XPRNProvider = ({ children, config, }) => {
405
414
  connect(true);
406
415
  }
407
416
  }, [config.restoreSession, connect, session]);
417
+ // Helper to clear identity proof
418
+ const clearIdentityProof = useCallback(() => {
419
+ setIdentityProof(null);
420
+ setIdentityProofStatus("idle");
421
+ }, []);
408
422
  const providerValue = useMemo(() => {
409
423
  return {
410
424
  // Config
@@ -417,14 +431,15 @@ export const XPRNProvider = ({ children, config, }) => {
417
431
  // Identity proof state
418
432
  identityProof,
419
433
  identityProofStatus,
420
- // Helper booleans
421
- needsIdentityProof,
422
- isIdentityProofRequired,
423
434
  isIdentityProofEnabled,
424
435
  // Core methods
425
436
  connect,
426
437
  disconnect,
427
438
  requestIdentityProof,
439
+ // Identity proof setters (for custom flows)
440
+ setIdentityProof,
441
+ clearIdentityProof,
442
+ setIdentityProofStatus,
428
443
  // Session switching
429
444
  listStoredSessions,
430
445
  switchToSession,
@@ -442,12 +457,11 @@ export const XPRNProvider = ({ children, config, }) => {
442
457
  profile,
443
458
  identityProof,
444
459
  identityProofStatus,
445
- needsIdentityProof,
446
- isIdentityProofRequired,
447
460
  isIdentityProofEnabled,
448
461
  connect,
449
462
  disconnect,
450
463
  requestIdentityProof,
464
+ clearIdentityProof,
451
465
  listStoredSessions,
452
466
  switchToSession,
453
467
  addTxError,
@@ -28,7 +28,15 @@ export async function createIdentityProof(session, options) {
28
28
  time: new Date().toISOString().slice(0, -1),
29
29
  });
30
30
  // Sign the transaction without broadcasting
31
- const txResult = await session.transact({ actions: [authenticationAction] }, { broadcast: false });
31
+ let txResult;
32
+ try {
33
+ txResult = await session.transact({ actions: [authenticationAction] }, { broadcast: false });
34
+ }
35
+ catch (error) {
36
+ // Re-throw with clearer context for wallet signing failures
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ throw new Error(`Wallet signing failed: ${message}`);
39
+ }
32
40
  // Check for abort after wallet interaction
33
41
  if (options?.signal?.aborted) {
34
42
  throw new DOMException("Aborted", "AbortError");
@@ -34,12 +34,8 @@ export type IdentityProofConfig = {
34
34
  validationUrl?: string;
35
35
  /** Time in seconds before expiration to trigger validation (default: 300 = 5 minutes) */
36
36
  validationBuffer?: number;
37
- /** Automatically authenticate on connect (default: false) */
38
- enforceOnConnect?: boolean;
39
37
  /** Automatically re-authenticate when token expires (default: false) */
40
38
  autoReauthenticate?: boolean;
41
- /** Require identity proof for the app - enables helper booleans and gate component (default: false) */
42
- required?: boolean;
43
39
  /** Additional headers for identity proof requests */
44
40
  headers?: Record<string, string>;
45
41
  /** Request timeout in milliseconds */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rockerone/xprnkit",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "source": "src/index.ts",
5
5
  "main": "build/index.js",
6
6
  "type": "module",