@rockerone/xprnkit 0.3.0 → 0.3.2
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 +57 -0
- package/build/components/xprn-transaction.js +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/providers/XPRNProvider.d.ts +4 -1
- package/build/providers/XPRNProvider.js +184 -134
- package/build/services/identity-proof/create-identity-proof.d.ts +23 -0
- package/build/services/identity-proof/create-identity-proof.js +47 -0
- package/build/services/identity-proof/index.d.ts +6 -0
- package/build/services/identity-proof/index.js +5 -0
- package/build/services/identity-proof/types.d.ts +62 -0
- package/build/services/identity-proof/types.js +1 -0
- package/build/services/identity-proof/use-identity-proof.d.ts +38 -0
- package/build/services/identity-proof/use-identity-proof.js +143 -0
- package/build/services/identity-proof/verify-identity-proof.d.ts +25 -0
- package/build/services/identity-proof/verify-identity-proof.js +59 -0
- package/build/services/index.d.ts +1 -0
- package/build/services/index.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,63 @@ Whether you're creating smart contracts, integrating wallets, or handling token
|
|
|
23
23
|
|
|
24
24
|
XPRNKit is the ideal toolkit for developers who want to build and deploy secure, scalable, and user-friendly dApps on XPRNetwork with greater efficiency.
|
|
25
25
|
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 0.3.1 Performance Update
|
|
29
|
+
|
|
30
|
+
Version 0.3.1 introduces significant performance optimizations to the `XPRNProvider` multi-session management system. The internal state architecture has been refactored to use refs instead of useState for session storage, dramatically reducing unnecessary re-renders.
|
|
31
|
+
|
|
32
|
+
### Performance Gains
|
|
33
|
+
|
|
34
|
+
| Scenario | Before | After | Improvement |
|
|
35
|
+
|----------|--------|-------|-------------|
|
|
36
|
+
| **Single-session usage** | baseline | optimized | **~1.5x faster** |
|
|
37
|
+
| **Multi-session (2-3 wallets)** | baseline | optimized | **~3-4x faster** |
|
|
38
|
+
| **Heavy multi-session (4+ wallets)** | baseline | optimized | **~5x+ faster** |
|
|
39
|
+
|
|
40
|
+
### Re-render Reduction (3-wallet scenario)
|
|
41
|
+
|
|
42
|
+
| Operation | Before | After | Reduction |
|
|
43
|
+
|-----------|--------|-------|-----------|
|
|
44
|
+
| Connect wallet 1 | 3 re-renders | 2 re-renders | 33% |
|
|
45
|
+
| Connect wallet 2 | 2 re-renders | 0 re-renders | 100% |
|
|
46
|
+
| Connect wallet 3 | 2 re-renders | 0 re-renders | 100% |
|
|
47
|
+
| Profile fetch (active) | 1 re-render | 1 re-render | 0% |
|
|
48
|
+
| Profile fetch (non-active) | 1 re-render | 0 re-renders | 100% |
|
|
49
|
+
| Authenticate (non-active) | 1 re-render | 0 re-renders | 100% |
|
|
50
|
+
| **Total (3-wallet setup)** | **13 re-renders** | **4 re-renders** | **~70%** |
|
|
51
|
+
|
|
52
|
+
### Callback Stability
|
|
53
|
+
|
|
54
|
+
All session management callbacks are now stable references:
|
|
55
|
+
|
|
56
|
+
| Callback | Dependencies | Stable? |
|
|
57
|
+
|----------|--------------|---------|
|
|
58
|
+
| `listSessions` | `[]` | ✅ Never recreated |
|
|
59
|
+
| `getSessionById` | `[]` | ✅ Never recreated |
|
|
60
|
+
| `getSessionByActor` | `[]` | ✅ Never recreated |
|
|
61
|
+
| `getAllProfiles` | `[]` | ✅ Never recreated |
|
|
62
|
+
| `getActiveSession` | `[]` | ✅ Never recreated |
|
|
63
|
+
| `setActiveSession` | `[forceUpdate]` | ✅ Stable |
|
|
64
|
+
| `switchSession` | `[setActiveSession]` | ✅ Stable |
|
|
65
|
+
| `removeSession` | `[config, forceUpdate]` | ✅ Stable |
|
|
66
|
+
| `disconnect` | `[removeSession]` | ✅ Stable |
|
|
67
|
+
| `authenticate` | `[config, forceUpdate]` | ✅ Stable |
|
|
68
|
+
| `connect` | `[config, forceUpdate]` | ✅ Stable |
|
|
69
|
+
|
|
70
|
+
### Key Optimizations
|
|
71
|
+
|
|
72
|
+
- **Ref-based session storage**: Sessions Map is now stored in a ref, eliminating Map recreation on every update
|
|
73
|
+
- **Selective re-renders**: Only triggers re-renders when the **active session** data changes
|
|
74
|
+
- **Zero re-renders for non-active operations**: Adding/updating non-active sessions doesn't cause re-renders
|
|
75
|
+
- **Stable callback references**: Consumer components using these callbacks won't re-render due to callback identity changes
|
|
76
|
+
|
|
77
|
+
### Backward Compatibility
|
|
78
|
+
|
|
79
|
+
These optimizations are fully backward compatible. All existing code using `useXPRN()` continues to work without changes.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
26
83
|
# XPRNProvider and useXPRN Documentation
|
|
27
84
|
|
|
28
85
|
## XPRNProvider
|
|
@@ -52,7 +52,7 @@ const XPRNTransaction = React.forwardRef(({ className, variant, size, asChild =
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}, [session, actions, setTxStatus]);
|
|
55
|
-
return (_jsxs(_Fragment, { children: [!session && (_jsx(Comp, { onClick: () => connect(false,
|
|
55
|
+
return (_jsxs(_Fragment, { children: [!session && (_jsx(Comp, { onClick: () => connect(false, () => pushTransaction()), className: cn(XPRNTransactionVariants({ variant, size, className })), ref: ref, children: "Connecte Me!", ...props })), session && (_jsx(Comp, { onClick: () => pushTransaction(), className: cn(XPRNTransactionVariants({ variant, size, className })), ref: ref, children: TxStatusNode, ...props }))] }));
|
|
56
56
|
});
|
|
57
57
|
XPRNTransaction.displayName = "XPRNTransaction";
|
|
58
58
|
export { XPRNTransaction, XPRNTransactionVariants };
|
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,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;
|
|
@@ -43,7 +44,7 @@ type XPRNProviderContext = {
|
|
|
43
44
|
authentication: XPRNAuthentication | null;
|
|
44
45
|
authenticate: (success: (res: any) => void, fail: (e: any) => void, actor?: string) => void;
|
|
45
46
|
addTransactionError: (rawMessage: string) => void;
|
|
46
|
-
connect: (restore?: boolean,
|
|
47
|
+
connect: (restore?: boolean, onSession?: (session: LinkSession) => void, onProfile?: (profile: XPRNProfile) => void) => void;
|
|
47
48
|
disconnect: (actor?: string) => void;
|
|
48
49
|
getActiveSession: () => XPRNSession | null;
|
|
49
50
|
listSessions: () => XPRNSession[];
|
|
@@ -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;
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { ApiClass } from "@proton/api";
|
|
4
4
|
import ConnectWallet from "@proton/web-sdk";
|
|
5
|
-
import React, { useCallback, useContext, useEffect, useMemo,
|
|
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 {
|
|
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(
|
|
17
|
+
addTransactionError() { },
|
|
18
18
|
authentication: null,
|
|
19
19
|
authenticate: () => { },
|
|
20
20
|
getActiveSession: () => null,
|
|
@@ -25,52 +25,80 @@ 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
|
-
//
|
|
31
|
-
const [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
32
|
+
// Force update mechanism - only triggers re-render when active session changes
|
|
33
|
+
const [version, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
34
|
+
// Internal refs - no re-renders when mutated
|
|
35
|
+
const sessionsRef = useRef(new Map());
|
|
36
|
+
const activeSessionIdRef = useRef(null);
|
|
37
|
+
const jsonRpcRef = useRef(new JsonRpc(config.endpoints));
|
|
38
|
+
const errorsStackRef = useRef([]);
|
|
36
39
|
const onSessionRef = useRef();
|
|
37
40
|
const onProfileRef = useRef();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
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());
|
|
46
|
+
// Derived values - recomputed only when version changes (active session affected)
|
|
47
|
+
const { activeSession, session, link, profile, authentication, authStatus } = useMemo(() => {
|
|
48
|
+
const active = activeSessionIdRef.current
|
|
49
|
+
? sessionsRef.current.get(activeSessionIdRef.current) ?? null
|
|
50
|
+
: null;
|
|
51
|
+
const status = activeSessionIdRef.current
|
|
52
|
+
? authStatusRef.current.get(activeSessionIdRef.current) ?? "idle"
|
|
53
|
+
: "idle";
|
|
54
|
+
return {
|
|
55
|
+
activeSession: active,
|
|
56
|
+
session: active?.session ?? null,
|
|
57
|
+
link: active?.link ?? null,
|
|
58
|
+
profile: active?.profile ?? null,
|
|
59
|
+
authentication: active?.authentication ?? null,
|
|
60
|
+
authStatus: status,
|
|
61
|
+
};
|
|
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
|
+
}, []);
|
|
68
|
+
// Session management methods - stable callbacks using refs
|
|
45
69
|
const getActiveSession = useCallback(() => {
|
|
46
|
-
return
|
|
47
|
-
|
|
70
|
+
return activeSessionIdRef.current
|
|
71
|
+
? sessionsRef.current.get(activeSessionIdRef.current) ?? null
|
|
72
|
+
: null;
|
|
73
|
+
}, []);
|
|
48
74
|
const listSessions = useCallback(() => {
|
|
49
|
-
return Array.from(
|
|
50
|
-
}, [
|
|
75
|
+
return Array.from(sessionsRef.current.values());
|
|
76
|
+
}, []);
|
|
51
77
|
const setActiveSession = useCallback((actor) => {
|
|
52
|
-
if (
|
|
53
|
-
|
|
78
|
+
if (sessionsRef.current.has(actor)) {
|
|
79
|
+
activeSessionIdRef.current = actor;
|
|
54
80
|
// Store in localStorage for persistence
|
|
55
81
|
if (typeof window !== "undefined") {
|
|
56
82
|
localStorage.setItem("xprn_active_session_actor", actor);
|
|
57
83
|
}
|
|
84
|
+
// Trigger re-render for consumers
|
|
85
|
+
forceUpdate();
|
|
58
86
|
}
|
|
59
87
|
else {
|
|
60
88
|
console.warn(`Session with actor ${actor} not found`);
|
|
61
89
|
}
|
|
62
|
-
}, [
|
|
90
|
+
}, [forceUpdate]);
|
|
63
91
|
const switchSession = useCallback((actor) => {
|
|
64
92
|
setActiveSession(actor);
|
|
65
93
|
}, [setActiveSession]);
|
|
66
94
|
const getSessionById = useCallback((actor) => {
|
|
67
|
-
return
|
|
68
|
-
}, [
|
|
95
|
+
return sessionsRef.current.get(actor) ?? null;
|
|
96
|
+
}, []);
|
|
69
97
|
const getSessionByActor = useCallback((actor) => {
|
|
70
|
-
return
|
|
71
|
-
}, [
|
|
98
|
+
return sessionsRef.current.get(actor) ?? null;
|
|
99
|
+
}, []);
|
|
72
100
|
const removeSession = useCallback(async (actor) => {
|
|
73
|
-
const sessionToRemove =
|
|
101
|
+
const sessionToRemove = sessionsRef.current.get(actor);
|
|
74
102
|
if (!sessionToRemove) {
|
|
75
103
|
console.warn(`Session with actor ${actor} not found`);
|
|
76
104
|
return;
|
|
@@ -82,25 +110,22 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
82
110
|
catch (e) {
|
|
83
111
|
console.error("Error removing session from link:", e);
|
|
84
112
|
}
|
|
85
|
-
// Remove from sessions map
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
// If this was the active session, clear it
|
|
92
|
-
if (activeSessionId === actor) {
|
|
93
|
-
setActiveSessionId(null);
|
|
113
|
+
// Remove from sessions map (mutate ref directly)
|
|
114
|
+
sessionsRef.current.delete(actor);
|
|
115
|
+
// If this was the active session, clear it and trigger re-render
|
|
116
|
+
if (activeSessionIdRef.current === actor) {
|
|
117
|
+
activeSessionIdRef.current = null;
|
|
94
118
|
if (typeof window !== "undefined") {
|
|
95
119
|
localStorage.removeItem("xprn_active_session_actor");
|
|
96
120
|
}
|
|
121
|
+
forceUpdate();
|
|
97
122
|
}
|
|
98
|
-
}, [
|
|
123
|
+
}, [config.requesterAccount, forceUpdate]);
|
|
99
124
|
const getAllProfiles = useCallback(() => {
|
|
100
|
-
return Array.from(
|
|
125
|
+
return Array.from(sessionsRef.current.values())
|
|
101
126
|
.map(s => s.profile)
|
|
102
127
|
.filter((p) => p !== null);
|
|
103
|
-
}, [
|
|
128
|
+
}, []);
|
|
104
129
|
const connect = useCallback((restoreSession, onSession, onProfile) => {
|
|
105
130
|
console.log("restoreSession", restoreSession);
|
|
106
131
|
ConnectWallet({
|
|
@@ -127,17 +152,15 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
127
152
|
profile: null,
|
|
128
153
|
authentication: null,
|
|
129
154
|
};
|
|
130
|
-
// Add to sessions map
|
|
131
|
-
|
|
132
|
-
const newSessions = new Map(prev);
|
|
133
|
-
newSessions.set(actor, newSession);
|
|
134
|
-
return newSessions;
|
|
135
|
-
});
|
|
155
|
+
// Add to sessions map (mutate ref directly)
|
|
156
|
+
sessionsRef.current.set(actor, newSession);
|
|
136
157
|
// Auto-switch to newly connected session
|
|
137
|
-
|
|
158
|
+
activeSessionIdRef.current = actor;
|
|
138
159
|
if (typeof window !== "undefined") {
|
|
139
160
|
localStorage.setItem("xprn_active_session_actor", actor);
|
|
140
161
|
}
|
|
162
|
+
// Trigger re-render since active session changed
|
|
163
|
+
forceUpdate();
|
|
141
164
|
// Fetch profile data
|
|
142
165
|
const api = new ApiClass(config.apiMode && config.apiMode == "testnet"
|
|
143
166
|
? "proton-test"
|
|
@@ -149,46 +172,44 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
149
172
|
avatar: `${profileRes.avatar}`,
|
|
150
173
|
isKyc: profileRes.kyc.length > 0 || false,
|
|
151
174
|
};
|
|
152
|
-
// Update session with profile
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
175
|
+
// Update session with profile (mutate ref)
|
|
176
|
+
const existingSession = sessionsRef.current.get(actor);
|
|
177
|
+
if (existingSession) {
|
|
178
|
+
sessionsRef.current.set(actor, {
|
|
179
|
+
...existingSession,
|
|
180
|
+
profile: xprProfile,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Only trigger re-render if this is still the active session
|
|
184
|
+
if (activeSessionIdRef.current === actor) {
|
|
185
|
+
forceUpdate();
|
|
186
|
+
}
|
|
164
187
|
}
|
|
165
188
|
});
|
|
166
189
|
}
|
|
167
190
|
});
|
|
168
|
-
}, [config]);
|
|
191
|
+
}, [config, forceUpdate]);
|
|
169
192
|
const disconnect = useCallback(async (actor) => {
|
|
170
193
|
// If actor is provided, disconnect that specific session
|
|
171
194
|
// Otherwise, disconnect the active session
|
|
172
|
-
const targetActor = actor ||
|
|
195
|
+
const targetActor = actor || activeSessionIdRef.current;
|
|
173
196
|
if (!targetActor) {
|
|
174
197
|
console.warn("No session to disconnect");
|
|
175
198
|
return;
|
|
176
199
|
}
|
|
177
200
|
await removeSession(targetActor);
|
|
178
|
-
}, [
|
|
201
|
+
}, [removeSession]);
|
|
179
202
|
const addTxError = useCallback((message) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}, [errorsStack, setErrorStack]);
|
|
184
|
-
const authenticate = useCallback((success, fail, actor) => {
|
|
203
|
+
errorsStackRef.current.push(parseTransactionErrorMessage(message));
|
|
204
|
+
}, []);
|
|
205
|
+
const authenticate = useCallback(async (success, fail, actor) => {
|
|
185
206
|
// Use provided actor or active session
|
|
186
|
-
const targetActor = actor ||
|
|
207
|
+
const targetActor = actor || activeSessionIdRef.current;
|
|
187
208
|
if (!targetActor) {
|
|
188
209
|
fail(new Error("No session available for authentication"));
|
|
189
210
|
return;
|
|
190
211
|
}
|
|
191
|
-
const targetSession =
|
|
212
|
+
const targetSession = sessionsRef.current.get(targetActor);
|
|
192
213
|
if (!targetSession) {
|
|
193
214
|
fail(new Error(`Session not found for actor: ${targetActor}`));
|
|
194
215
|
return;
|
|
@@ -197,69 +218,77 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
197
218
|
fail(new Error("Authentication URL not configured"));
|
|
198
219
|
return;
|
|
199
220
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
};
|
|
209
238
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// Update session with authentication data
|
|
235
|
-
setSessions(prev => {
|
|
236
|
-
const newSessions = new Map(prev);
|
|
237
|
-
const existingSession = newSessions.get(targetActor);
|
|
238
|
-
if (existingSession) {
|
|
239
|
-
newSessions.set(targetActor, {
|
|
240
|
-
...existingSession,
|
|
241
|
-
authentication: authData,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return newSessions;
|
|
245
|
-
});
|
|
246
|
-
success(authRes);
|
|
247
|
-
})
|
|
248
|
-
.catch(fail);
|
|
249
|
-
})
|
|
250
|
-
.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);
|
|
251
263
|
}
|
|
252
|
-
catch (
|
|
253
|
-
|
|
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);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
authenticatingRef.current.delete(targetActor);
|
|
276
|
+
if (authAbortControllersRef.current.get(targetActor) === abortController) {
|
|
277
|
+
authAbortControllersRef.current.delete(targetActor);
|
|
278
|
+
}
|
|
254
279
|
}
|
|
255
|
-
}, [
|
|
280
|
+
}, [config.authenticationUrl, forceUpdate]);
|
|
256
281
|
// Handle authentication for active session
|
|
257
282
|
useEffect(() => {
|
|
258
|
-
if (session &&
|
|
259
|
-
const currentSession =
|
|
283
|
+
if (session && activeSessionIdRef.current) {
|
|
284
|
+
const currentSession = sessionsRef.current.get(activeSessionIdRef.current);
|
|
260
285
|
if (currentSession && !currentSession.authentication) {
|
|
261
286
|
if (config.enforceAuthentication && config.authenticationUrl) {
|
|
262
|
-
authenticate(() => {
|
|
287
|
+
authenticate(() => {
|
|
288
|
+
console.log(`[XPRNProvider] Auto-authenticated: ${activeSessionIdRef.current}`);
|
|
289
|
+
}, error => {
|
|
290
|
+
console.error("[XPRNProvider] Auto-authentication failed:", error);
|
|
291
|
+
});
|
|
263
292
|
}
|
|
264
293
|
}
|
|
265
294
|
}
|
|
@@ -267,7 +296,19 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
267
296
|
onSessionRef.current(session);
|
|
268
297
|
onSessionRef.current = undefined; // Reset the ref
|
|
269
298
|
}
|
|
270
|
-
}, [
|
|
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
|
+
}, []);
|
|
271
312
|
// Handle profile callbacks
|
|
272
313
|
useEffect(() => {
|
|
273
314
|
if (profile && onProfileRef.current) {
|
|
@@ -275,32 +316,35 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
275
316
|
onProfileRef.current = undefined; // Reset the ref
|
|
276
317
|
}
|
|
277
318
|
}, [profile]);
|
|
278
|
-
// Session restoration - only restore active session
|
|
319
|
+
// Session restoration - only restore active session (runs once on mount)
|
|
279
320
|
useEffect(() => {
|
|
280
|
-
if (
|
|
321
|
+
if (isRestoringRef.current)
|
|
322
|
+
return;
|
|
323
|
+
if (sessionsRef.current.size === 0 && config.restoreSession) {
|
|
281
324
|
// Check if there's a stored active session
|
|
282
325
|
if (typeof window !== "undefined") {
|
|
283
326
|
const storedActiveActor = localStorage.getItem("xprn_active_session_actor");
|
|
284
327
|
if (storedActiveActor) {
|
|
328
|
+
isRestoringRef.current = true;
|
|
285
329
|
// Try to restore the session
|
|
286
330
|
connect(true);
|
|
287
331
|
}
|
|
288
332
|
}
|
|
289
333
|
}
|
|
290
|
-
}, [
|
|
334
|
+
}, [config.restoreSession, connect]);
|
|
291
335
|
const providerValue = useMemo(() => {
|
|
292
336
|
return {
|
|
293
|
-
// Backward compatibility - active session values
|
|
337
|
+
// Backward compatibility - active session values (reactive via version)
|
|
294
338
|
session,
|
|
295
339
|
link,
|
|
296
340
|
profile,
|
|
297
|
-
rpc:
|
|
341
|
+
rpc: jsonRpcRef.current,
|
|
298
342
|
addTransactionError: addTxError,
|
|
299
343
|
connect,
|
|
300
344
|
disconnect,
|
|
301
345
|
authenticate,
|
|
302
346
|
authentication,
|
|
303
|
-
// Multi-session management methods
|
|
347
|
+
// Multi-session management methods (stable references)
|
|
304
348
|
getActiveSession,
|
|
305
349
|
listSessions,
|
|
306
350
|
setActiveSession,
|
|
@@ -309,17 +353,22 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
309
353
|
switchSession,
|
|
310
354
|
getSessionById,
|
|
311
355
|
getSessionByActor,
|
|
356
|
+
// Authentication status
|
|
357
|
+
authStatus,
|
|
358
|
+
getAuthStatus,
|
|
312
359
|
};
|
|
313
360
|
}, [
|
|
361
|
+
// Reactive values (change when active session changes)
|
|
314
362
|
session,
|
|
315
363
|
link,
|
|
316
364
|
profile,
|
|
317
|
-
|
|
365
|
+
authentication,
|
|
366
|
+
authStatus,
|
|
367
|
+
// Stable callbacks (only change when their specific deps change)
|
|
318
368
|
addTxError,
|
|
319
369
|
connect,
|
|
320
370
|
disconnect,
|
|
321
371
|
authenticate,
|
|
322
|
-
authentication,
|
|
323
372
|
getActiveSession,
|
|
324
373
|
listSessions,
|
|
325
374
|
setActiveSession,
|
|
@@ -328,6 +377,7 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
328
377
|
switchSession,
|
|
329
378
|
getSessionById,
|
|
330
379
|
getSessionByActor,
|
|
380
|
+
getAuthStatus,
|
|
331
381
|
]);
|
|
332
382
|
return (_jsx(XPRNContext.Provider, { value: providerValue, children: children }));
|
|
333
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,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";
|
package/package.json
CHANGED