├── .github
├── ISSUE_TEMPLATES
│ ├── bug_report.md
│ ├── ci.md
│ ├── documentation.md
│ └── feature-request.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── run-test-build-deploy.yaml
├── .gitignore
├── README.md
├── commitlint.config.cjs
├── example
├── .gitignore
├── README.md
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── src
│ └── app
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── page.tsx
├── tailwind.config.ts
└── tsconfig.json
├── package-lock.json
├── package.json
├── readme
├── depemdency-diagram.svg
├── usage-flow-2.svg
├── usage-flow-3.svg
└── usage-flow.svg
├── scripts
└── build.sh
├── src
├── Reclaim.ts
├── contract-types
│ ├── config.json
│ ├── contracts
│ │ ├── factories
│ │ │ ├── Reclaim__factory.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ └── index.ts
├── index.ts
├── smart-contract.ts
├── utils
│ ├── __tests__
│ │ └── helper.test.ts
│ ├── constants.ts
│ ├── device.ts
│ ├── errors.ts
│ ├── helper.ts
│ ├── interfaces.ts
│ ├── logger.ts
│ ├── modalUtils.ts
│ ├── proofUtils.ts
│ ├── sessionUtils.ts
│ ├── types.ts
│ └── validationUtils.ts
└── witness.ts
└── tsconfig.json
/.github/ISSUE_TEMPLATES/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 General Bug Report
3 | about: Report an issue to help improve the project.
4 | title: ''
5 | labels: 'kind/bug'
6 | assignees: ''
7 | ---
8 |
9 | ### Current Behavior
10 |
11 |
12 | ### Expected Behavior
13 |
14 |
15 | ### Screenshots/Logs
16 |
17 |
18 | ### To Reproduce
19 | 1. Go to '...'
20 | 2. Click on '....'
21 | 3. Scroll down to '....'
22 | 4. See error
23 |
24 | ---
25 | ## Before You Begin
26 |
27 | Before submitting an issue, please ensure you have completed the following steps:
28 |
29 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md).
30 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md).
31 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md).
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATES/ci.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🛠 Continuous Integration / DevOps
3 | about: Improve or update workflows or other automation
4 | title: '[CI] '
5 | labels: 'area/ci'
6 | assignees: ''
7 | ---
8 |
9 | ## Issue Description
10 |
11 | ### Current Situation
12 |
13 |
14 | ### Desired Outcome
15 |
16 |
17 | ### Proposed Implementation
18 |
19 |
20 | ### Additional Context
21 |
22 |
23 | ---
24 | ## Before You Begin
25 |
26 | Before submitting an issue, please ensure you have completed the following steps:
27 |
28 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md).
29 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md).
30 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md).
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATES/documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 📄 Documentation issue
3 | about: Issues related to documentation.
4 | title: '[Docs]'
5 | labels: 'area/docs, language/markdown'
6 | assignees: ''
7 | ---
8 |
9 | ### Current State
10 |
11 |
12 | ### Desired State
13 |
14 |
15 | ---
16 | ## Before You Begin
17 |
18 | Before submitting an issue, please ensure you have completed the following steps:
19 |
20 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md).
21 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md).
22 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md).
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATES/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 💡 Feature Request
3 | about: Suggest an enhancement for this project
4 | title: '[FEATURE] '
5 | labels: 'enhancement'
6 | assignees: ''
7 | ---
8 |
9 | ## Feature Description
10 |
11 | ### Current Situation
12 |
13 |
14 | ### Proposed Enhancement
15 |
16 |
17 | ### Benefits (Optional)
18 |
19 |
20 | ### Possible Implementation (Optional)
21 |
22 |
23 |
24 | ### Additional Context
25 |
26 |
27 | ---
28 | ## Before You Begin
29 |
30 | Before submitting an issue, please ensure you have completed the following steps:
31 |
32 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md).
33 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md).
34 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md).
35 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Description
2 |
3 |
4 | ### Testing (ignore for documentation update)
5 |
6 |
7 | ### Type of change
8 | - [ ] Bug fix
9 | - [ ] New feature
10 | - [ ] Breaking change
11 | - [ ] Documentation update
12 |
13 | ### Checklist:
14 | - [ ] I have read and agree to the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md).
15 | - [ ] I have signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md).
16 | - [ ] I have considered the [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md).
17 | - [ ] I have self-reviewed and tested my code.
18 | - [ ] I have updated documentation as needed.
19 | - [ ] I agree to the [project's custom license](https://github.com/reclaimprotocol/.github/blob/main/LICENSE).
20 |
21 | ### Additional Notes:
22 |
23 |
--------------------------------------------------------------------------------
/.github/workflows/run-test-build-deploy.yaml:
--------------------------------------------------------------------------------
1 | name: build-deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | workflow_dispatch:
8 | inputs:
9 | release_type:
10 | description: "Type of release"
11 | required: true
12 | default: "patch"
13 | type: choice
14 | options:
15 | - patch
16 | - minor
17 | - major
18 | - bugfix
19 | - hotfix
20 |
21 | permissions:
22 | contents: write
23 | packages: write
24 |
25 | jobs:
26 | test_and_build:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - uses: actions/checkout@v3
30 |
31 | - name: Set up Node.js
32 | uses: actions/setup-node@v3
33 | with:
34 | node-version: "16"
35 |
36 | - name: Clean install dependencies
37 | run: |
38 | rm -rf node_modules
39 | rm -rf package-lock.json
40 | npm i
41 |
42 | - name: Build package
43 | run: npm run build
44 |
45 | - name: Upload build artifacts
46 | uses: actions/upload-artifact@v4
47 | with:
48 | name: dist
49 | path: dist/
50 |
51 | publish:
52 | needs: test_and_build
53 | runs-on: ubuntu-latest
54 | if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'
55 | steps:
56 | - uses: actions/checkout@v3
57 | with:
58 | persist-credentials: false # Disable automatic token authentication
59 |
60 | - name: Set up Node.js
61 | uses: actions/setup-node@v3
62 | with:
63 | node-version: "16"
64 | registry-url: "https://registry.npmjs.org"
65 |
66 | - name: Download build artifacts
67 | uses: actions/download-artifact@v4
68 | with:
69 | name: dist
70 | path: dist/
71 |
72 | - name: Version and publish
73 | env:
74 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
75 | run: |
76 | npm publish
77 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | dist
4 |
5 | .env*
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
7 | # Reclaim Protocol JavaScript SDK Integration Guide
8 |
9 | This guide will walk you through integrating the Reclaim Protocol JavaScript SDK into your application. We'll create a simple React application that demonstrates how to use the SDK to generate proofs and verify claims.
10 |
11 | ## Prerequisites
12 |
13 | Before we begin, make sure you have:
14 |
15 | 1. An application ID from Reclaim Protocol.
16 | 2. An application secret from Reclaim Protocol.
17 | 3. A provider ID for the specific service you want to verify.
18 |
19 | You can obtain these details from the [Reclaim Developer Portal](https://dev.reclaimprotocol.org/).
20 |
21 | ## Step 1: Create a new React application
22 |
23 | Let's start by creating a new React application:
24 |
25 | ```bash
26 | npx create-react-app reclaim-app
27 | cd reclaim-app
28 | ```
29 |
30 | ## Step 2: Install necessary dependencies
31 |
32 | Install the Reclaim Protocol SDK and a QR code generator:
33 |
34 | ```bash
35 | npm install @reclaimprotocol/js-sdk react-qr-code
36 | ```
37 |
38 | ## Step 3: Set up your React component
39 |
40 | Replace the contents of `src/App.js` with the following code:
41 |
42 | ```javascript
43 | import React, { useState, useEffect } from 'react'
44 | import { ReclaimProofRequest } from '@reclaimprotocol/js-sdk'
45 | import QRCode from 'react-qr-code'
46 |
47 | function App() {
48 | const [reclaimProofRequest, setReclaimProofRequest] = useState(null)
49 | const [requestUrl, setRequestUrl] = useState('')
50 | const [statusUrl, setStatusUrl] = useState('')
51 | const [proofs, setProofs] = useState(null)
52 |
53 | useEffect(() => {
54 | async function initializeReclaim() {
55 | const APP_ID = 'YOUR_APPLICATION_ID_HERE'
56 | const APP_SECRET = 'YOUR_APPLICATION_SECRET_HERE'
57 | const PROVIDER_ID = 'YOUR_PROVIDER_ID_HERE'
58 |
59 | const proofRequest = await ReclaimProofRequest.init(
60 | APP_ID,
61 | APP_SECRET,
62 | PROVIDER_ID
63 | )
64 | setReclaimProofRequest(proofRequest)
65 | }
66 |
67 | initializeReclaim()
68 | }, [])
69 |
70 | async function handleCreateClaim() {
71 | if (!reclaimProofRequest) {
72 | console.error('Reclaim Proof Request not initialized')
73 | return
74 | }
75 |
76 | const url = await reclaimProofRequest.getRequestUrl()
77 | setRequestUrl(url)
78 |
79 | const status = reclaimProofRequest.getStatusUrl()
80 | setStatusUrl(status)
81 | console.log('Status URL:', status)
82 |
83 | await reclaimProofRequest.startSession({
84 | onSuccess: (proofs) => {
85 | if (proofs && typeof proofs === 'string') {
86 | // When using a custom callback url, the proof is returned to the callback url and we get a message instead of a proof
87 | console.log('SDK Message:', proofs)
88 | setProofs(proofs)
89 | } else if (proofs && typeof proofs !== 'string') {
90 | // When using the default callback url, we get a proof
91 | if (Array.isArray(proofs)) {
92 | // when using the cascading providers, providers having more than one proof will return an array of proofs
93 | console.log(JSON.stringify(proofs.map(p => p.claimData.context)))
94 | } else {
95 | console.log('Proof received:', proofs?.claimData.context)
96 | }
97 | setProofs(proofs)
98 | }
99 | },
100 | onFailure: (error) => {
101 | console.error('Verification failed', error)
102 | }
103 | })
104 | }
105 |
106 | return (
107 |
108 |
Reclaim Protocol Demo
109 |
110 | {requestUrl && (
111 |
112 |
Scan this QR code to start the verification process:
113 |
114 |
115 | )}
116 | {proofs && (
117 |
118 |
Verification Successful!
119 |
{JSON.stringify(proofs, null, 2)}
120 |
121 | )}
122 |
123 | )
124 | }
125 |
126 | export default App
127 | ```
128 |
129 | ## Step 4: Understanding the code
130 |
131 | Let's break down what's happening in this code:
132 |
133 | 1. We initialize the Reclaim SDK with your application ID, secret, and provider ID. This happens once when the component mounts.
134 |
135 | 2. When the user clicks the "Create Claim" button, we:
136 | - Generate a request URL using `getRequestUrl()`. This URL is used to create the QR code.
137 | - Get the status URL using `getStatusUrl()`. This URL can be used to check the status of the claim process.
138 | - Start a session with `startSession()`, which sets up callbacks for successful and failed verifications.
139 |
140 | 3. We display a QR code using the request URL. When a user scans this code, it starts the verification process.
141 |
142 | 4. The status URL is logged to the console. You can use this URL to check the status of the claim process programmatically.
143 |
144 | 5. When the verification is successful, we display the proof data on the page.
145 |
146 | ## Step 5: New Streamlined Flow with Browser Extension Support
147 |
148 | The Reclaim SDK now provides a simplified `triggerReclaimFlow()` method that automatically handles the verification process across different platforms and devices. This method intelligently chooses the best verification method based on the user's environment.
149 |
150 | ### Using triggerReclaimFlow()
151 |
152 | Replace the `handleCreateClaim` function in your React component with this simpler approach:
153 |
154 | ```javascript
155 | async function handleCreateClaim() {
156 | if (!reclaimProofRequest) {
157 | console.error('Reclaim Proof Request not initialized')
158 | return
159 | }
160 |
161 | try {
162 | // Start the verification process automatically
163 | await reclaimProofRequest.triggerReclaimFlow()
164 |
165 | // Listen for the verification results
166 | await reclaimProofRequest.startSession({
167 | onSuccess: (proofs) => {
168 | if (proofs && typeof proofs === 'string') {
169 | console.log('SDK Message:', proofs)
170 | setProofs(proofs)
171 | } else if (proofs && typeof proofs !== 'string') {
172 | if (Array.isArray(proofs)) {
173 | console.log(JSON.stringify(proofs.map(p => p.claimData.context)))
174 | } else {
175 | console.log('Proof received:', proofs?.claimData.context)
176 | }
177 | setProofs(proofs)
178 | }
179 | },
180 | onFailure: (error) => {
181 | console.error('Verification failed', error)
182 | }
183 | })
184 | } catch (error) {
185 | console.error('Error triggering Reclaim flow:', error)
186 | }
187 | }
188 | ```
189 |
190 | ### How triggerReclaimFlow() Works
191 |
192 | The `triggerReclaimFlow()` method automatically detects the user's environment and chooses the optimal verification method:
193 |
194 | #### On Desktop Browsers:
195 | 1. **Browser Extension First**: If the Reclaim browser extension is installed, it will use the extension for a seamless in-browser verification experience.
196 | 2. **QR Code Fallback**: If the extension is not available, it automatically displays a QR code modal for mobile scanning.
197 |
198 | #### On Mobile Devices:
199 | 1. **iOS Devices**: Automatically redirects to the Reclaim App Clip for native iOS verification.
200 | 2. **Android Devices**: Automatically redirects to the Reclaim Instant App for native Android verification.
201 |
202 | ### Browser Extension Support
203 |
204 | The SDK now includes built-in support for the Reclaim browser extension, providing users with a seamless verification experience without leaving their current browser tab.
205 |
206 | #### Features:
207 | - **Automatic Detection**: The SDK automatically detects if the Reclaim browser extension is installed
208 | - **Seamless Integration**: No additional setup required - the extension integration works out of the box
209 | - **Fallback Support**: If the extension is not available, the SDK gracefully falls back to QR code or mobile app flows
210 |
211 | #### Manual Extension Detection:
212 |
213 | You can also manually check if the browser extension is available:
214 |
215 | ```javascript
216 | const isExtensionAvailable = await reclaimProofRequest.isBrowserExtensionAvailable()
217 | if (isExtensionAvailable) {
218 | console.log('Reclaim browser extension is installed')
219 | } else {
220 | console.log('Browser extension not available, will use alternative flow')
221 | }
222 | ```
223 |
224 | #### Configuring Browser Extension Options:
225 |
226 | You can customize the browser extension behavior during SDK initialization:
227 |
228 | ```javascript
229 | const proofRequest = await ReclaimProofRequest.init(
230 | APP_ID,
231 | APP_SECRET,
232 | PROVIDER_ID,
233 | {
234 | useBrowserExtension: true, // Enable/disable browser extension (default: true)
235 | extensionID: 'custom-extension-id', // Use custom extension ID if needed
236 | // ... other options
237 | }
238 | )
239 | ```
240 |
241 | ### Modal Customization
242 |
243 | When the QR code modal is displayed (fallback on desktop), you can customize its appearance:
244 |
245 | ```javascript
246 | // Set modal options before triggering the flow
247 | reclaimProofRequest.setModalOptions({
248 | title: 'Custom Verification Title',
249 | description: 'Scan this QR code with your mobile device to verify your account',
250 | darkTheme: true, // Enable dark theme
251 | extensionUrl: 'https://custom-extension-url.com' // Custom extension download URL
252 | })
253 |
254 | await reclaimProofRequest.triggerReclaimFlow()
255 | ```
256 |
257 | ### Benefits of the New Flow:
258 |
259 | 1. **Platform Adaptive**: Automatically chooses the best verification method for each platform
260 | 2. **User-Friendly**: Provides the most seamless experience possible for each user
261 | 3. **Simplified Integration**: Single method call handles all verification scenarios
262 | 4. **Extension Support**: Leverages browser extension for desktop users when available
263 | 5. **Mobile Optimized**: Native app experiences on mobile devices
264 |
265 | ## Step 6: Run your application
266 |
267 | Start your development server:
268 |
269 | ```bash
270 | npm start
271 | ```
272 |
273 | Your Reclaim SDK demo should now be running. Click the "Create Claim" button to generate a QR code. Scan this code to start the verification process.
274 |
275 | ## Understanding the Claim Process
276 |
277 | 1. **Creating a Claim**: When you click "Create Claim", the SDK generates a unique request for verification.
278 |
279 | 2. **QR Code**: The QR code contains the request URL. When scanned, it initiates the verification process.
280 |
281 | 3. **Status URL**: This URL (logged to the console) can be used to check the status of the claim process. It's useful for tracking the progress of verification.
282 |
283 | 4. **Verification**: The `onSuccess` is called when verification is successful, providing the proof data. When using a custom callback url, the proof is returned to the callback url and we get a message instead of a proof.
284 |
285 | 5. **Handling Failures**: The `onFailure` is called if verification fails, allowing you to handle errors gracefully.
286 |
287 | ## Advanced Configuration
288 |
289 | The Reclaim SDK offers several advanced options to customize your integration:
290 |
291 | 1. **Adding Context**:
292 | You can add context to your proof request, which can be useful for providing additional information:
293 | ```javascript
294 | reclaimProofRequest.addContext('0x00000000000', 'Example context message')
295 | ```
296 |
297 | 2. **Setting Parameters**:
298 | If your provider requires specific parameters, you can set them like this:
299 | ```javascript
300 | reclaimProofRequest.setParams({ email: "test@example.com", userName: "testUser" })
301 | ```
302 |
303 | 3. **Custom Redirect URL**:
304 | Set a custom URL to redirect users after the verification process:
305 | ```javascript
306 | reclaimProofRequest.setRedirectUrl('https://example.com/redirect')
307 | ```
308 |
309 | 4. **Custom Callback URL**:
310 | Set a custom callback URL for your app which allows you to receive proofs and status updates on your callback URL:
311 | Pass in `jsonProofResponse: true` to receive the proof in JSON format: By default, the proof is returned as a url encoded string.
312 | ```javascript
313 | reclaimProofRequest.setAppCallbackUrl('https://example.com/callback', true)
314 | ```
315 |
316 | 5. **Modal Customization for Desktop Users**:
317 | Customize the appearance and behavior of the QR code modal shown to desktop users:
318 | ```javascript
319 | reclaimProofRequest.setModalOptions({
320 | title: 'Verify Your Account',
321 | description: 'Scan the QR code with your mobile device or install our browser extension',
322 | darkTheme: false, // Enable dark theme (default: false)
323 | extensionUrl: 'https://chrome.google.com/webstore/detail/reclaim' // Custom extension URL
324 | })
325 | ```
326 |
327 | 6. **Browser Extension Configuration**:
328 | Configure browser extension behavior and detection:
329 | ```javascript
330 | // Check if browser extension is available
331 | const isExtensionAvailable = await reclaimProofRequest.isBrowserExtensionAvailable()
332 |
333 | // Trigger the verification flow with automatic platform detection
334 | await reclaimProofRequest.triggerReclaimFlow()
335 |
336 | // Initialize with browser extension options
337 | const proofRequest = await ReclaimProofRequest.init(
338 | APP_ID,
339 | APP_SECRET,
340 | PROVIDER_ID,
341 | {
342 | useBrowserExtension: true, // Enable browser extension support (default: true)
343 | extensionID: 'custom-extension-id', // Custom extension identifier
344 | useAppClip: true, // Enable mobile app clips (default: true)
345 | log: true // Enable logging for debugging
346 | }
347 | )
348 | ```
349 |
350 | 7. **Platform-Specific Flow Control**:
351 | The `triggerReclaimFlow()` method provides intelligent platform detection, but you can still use traditional methods for custom flows:
352 | ```javascript
353 | // Traditional approach with manual QR code handling
354 | const requestUrl = await reclaimProofRequest.getRequestUrl()
355 | // Display your own QR code implementation
356 |
357 | // Or use the new streamlined approach
358 | await reclaimProofRequest.triggerReclaimFlow()
359 | // Automatically handles platform detection and optimal user experience
360 | ```
361 |
362 | 8. **Exporting and Importing SDK Configuration**:
363 | You can export the entire Reclaim SDK configuration as a JSON string and use it to initialize the SDK with the same configuration on a different service or backend:
364 | ```javascript
365 | // On the client-side or initial service
366 | const configJson = reclaimProofRequest.toJsonString()
367 | console.log('Exportable config:', configJson)
368 |
369 | // Send this configJson to your backend or another service
370 |
371 | // On the backend or different service
372 | const importedRequest = ReclaimProofRequest.fromJsonString(configJson)
373 | const requestUrl = await importedRequest.getRequestUrl()
374 | ```
375 | This allows you to generate request URLs and other details from your backend or a different service while maintaining the same configuration.
376 |
377 | ## Handling Proofs on Your Backend
378 |
379 | For production applications, it's recommended to handle proofs on your backend:
380 |
381 | 1. Set a callback URL:
382 | ```javascript
383 | reclaimProofRequest.setCallbackUrl('https://your-backend.com/receive-proofs')
384 | ```
385 |
386 | These options allow you to securely process proofs and status updates on your server.
387 |
388 | ## Next Steps
389 |
390 | Explore the [Reclaim Protocol documentation](https://docs.reclaimprotocol.org/) for more advanced features and best practices for integrating the SDK into your production applications.
391 |
392 | Happy coding with Reclaim Protocol!
393 |
394 | ## Contributing to Our Project
395 |
396 | We welcome contributions to our project! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request.
397 |
398 | ## Security Note
399 |
400 | Always keep your Application Secret secure. Never expose it in client-side code or public repositories.
401 |
402 | ## Code of Conduct
403 |
404 | Please read and follow our [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md) to ensure a positive and inclusive environment for all contributors.
405 |
406 | ## Security
407 |
408 | If you discover any security-related issues, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md) for information on how to responsibly disclose vulnerabilities.
409 |
410 | ## Contributor License Agreement
411 |
412 | Before contributing to this project, please read and sign our [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md).
413 |
414 | ## Indie Hackers
415 |
416 | For Indie Hackers: [Check out our guidelines and potential grant opportunities](https://github.com/reclaimprotocol/.github/blob/main/Indie-Hackers.md)
417 |
418 | ## License
419 |
420 | This project is licensed under a [custom license](https://github.com/reclaimprotocol/.github/blob/main/LICENSE). By contributing to this project, you agree that your contributions will be licensed under its terms.
421 |
422 | Thank you for your contributions!
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | };
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/example/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@reclaimprotocol/js-sdk": "file:..",
13 | "next": "14.1.0",
14 | "next-qrcode": "^2.5.1",
15 | "qrcode": "^1.5.3",
16 | "qrcode-next": "^1.4.5",
17 | "react": "^18",
18 | "react-dom": "^18",
19 | "uuid": "^9.0.1"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^20",
23 | "@types/qrcode": "^1.5.5",
24 | "@types/react": "^18",
25 | "@types/react-dom": "^18",
26 | "autoprefixer": "^10.0.1",
27 | "postcss": "^8",
28 | "tailwindcss": "^3.3.0",
29 | "typescript": "^5"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/example/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reclaimprotocol/reclaim-js-sdk/eff578902cfbd48b6193b0c0ca6eeeaf93c3b99d/example/src/app/favicon.ico
--------------------------------------------------------------------------------
/example/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | /* @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | } */
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | /* background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb)); */
27 | }
28 |
29 | @layer utilities {
30 | .text-balance {
31 | text-wrap: balance;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/example/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Create Next App",
9 | description: "Generated by create next app",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/example/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect, useState } from 'react'
3 | import { ReclaimProofRequest } from '@reclaimprotocol/js-sdk'
4 | import { Proof } from '@reclaimprotocol/js-sdk'
5 |
6 | export default function Home() {
7 | const [isLoading, setIsLoading] = useState(false)
8 | const [proofData, setProofData] = useState(null)
9 | const [error, setError] = useState(null)
10 | const [reclaimProofRequest, setReclaimProofRequest] = useState(null)
11 |
12 | useEffect(() => {
13 | // Initialize the ReclaimProofRequest when the component mounts
14 | initializeReclaimProofRequest()
15 | // verifyProofData()
16 | }, [])
17 |
18 | async function initializeReclaimProofRequest() {
19 | try {
20 | // ReclaimProofRequest Fields:
21 | // - applicationId: Unique identifier for your application
22 | // - providerId: Identifier for the specific provider you're using
23 | // - sessionId: Unique identifier for the current proof request session
24 | // - context: Additional context information for the proof request
25 | // - requestedProof: Details of the proof being requested
26 | // - signature: Cryptographic signature for request authentication
27 | // - redirectUrl: URL to redirect after proof generation (optional)
28 | // - appCallbackUrl: URL for receiving proof generation updates (optional)
29 | // - timeStamp: Timestamp of the proof request
30 | // - options: Additional configuration options
31 |
32 |
33 | const proofRequest = await ReclaimProofRequest.init(
34 | process.env.NEXT_PUBLIC_RECLAIM_APP_ID!,
35 | process.env.NEXT_PUBLIC_RECLAIM_APP_SECRET!,
36 | process.env.NEXT_PUBLIC_RECLAIM_PROVIDER_ID!,
37 | // Uncomment the following line to enable logging and AI providers
38 | {
39 | log: true,
40 | }
41 | )
42 | setReclaimProofRequest(proofRequest)
43 |
44 | // // // Add context to the proof request (optional)
45 | // proofRequest.addContext("0x48796C654F7574707574", "test")
46 |
47 | // Set parameters for the proof request (if needed)
48 | // proofRequest.setParams({ email: "test@example.com", userName: "testUser" })
49 |
50 | // Set a redirect URL (if needed)
51 | // proofRequest.setRedirectUrl('https://example.com/redirect')
52 |
53 | // Set a custom app callback URL (if needed)
54 | // proofRequest.setAppCallbackUrl('https://webhook.site/fd6cf442-0ea7-4427-8cb8-cb4dbe8884d2')
55 |
56 | // Uncomment the following line to log the proof request and to get the Json String
57 | // console.log('Proof request initialized:', proofRequest.toJsonString())
58 | } catch (error) {
59 | console.error('Error initializing ReclaimProofRequest:', error)
60 | setError('Failed to initialize Reclaim. Please try again.')
61 | }
62 | }
63 |
64 | async function startClaimProcess() {
65 | if (!reclaimProofRequest) {
66 | setError('Reclaim not initialized. Please refresh the page.')
67 | return
68 | }
69 |
70 | setIsLoading(true)
71 | setError(null)
72 |
73 | try {
74 | // Start the verification session
75 | await reclaimProofRequest.triggerReclaimFlow()
76 |
77 | await reclaimProofRequest.startSession({
78 | onSuccess: async (proof: Proof | Proof[] | string | undefined) => {
79 | setIsLoading(false)
80 |
81 | if (proof && typeof proof === 'string') {
82 | console.log('SDK Message:', proof)
83 | setError('Received string response instead of proof object.')
84 | } else if (proof && typeof proof !== 'string') {
85 | console.log('Proof received:', proof)
86 | if (Array.isArray(proof)) {
87 | setProofData(proof)
88 | } else {
89 | setProofData([proof])
90 | }
91 | }
92 | },
93 | onError: (error: Error) => {
94 | console.error('Error in proof generation:', error)
95 | setIsLoading(false)
96 | setError(`Error: ${error.message}`)
97 | }
98 | })
99 | } catch (error) {
100 | console.error('Error starting verification session:', error)
101 | setIsLoading(false)
102 | setError('Failed to start verification. Please try again.')
103 | }
104 | }
105 |
106 | // Function to extract provider URL from parameters
107 | const getProviderUrl = (proof: Proof) => {
108 | try {
109 | const parameters = JSON.parse(proof.claimData.parameters);
110 | return parameters.url || "Unknown Provider";
111 | } catch (e) {
112 | return proof.claimData.provider || "Unknown Provider";
113 | }
114 | }
115 |
116 | // Function to beautify and display extracted parameters
117 | const renderExtractedParameters = (proof: Proof) => {
118 | try {
119 | const context = JSON.parse(proof.claimData.context)
120 | const extractedParams = context.extractedParameters || {}
121 |
122 | return (
123 | <>
124 | Extracted Parameters
125 | {Object.entries(extractedParams).length > 0 ? (
126 |
127 | {Object.entries(extractedParams).map(([key, value]) => (
128 |
129 |
130 | {key}:
131 | {String(value)}
132 |
133 |
134 | ))}
135 |
136 | ) : (
137 | No parameters extracted
138 | )}
139 | >
140 | )
141 | } catch (e) {
142 | return Failed to parse parameters
143 | }
144 | }
145 |
146 | return (
147 |
148 |
149 |
Reclaim SDK
150 |
151 | {!proofData && !isLoading && (
152 |
153 |
154 | Click the button below to start the claim process.
155 |
156 |
163 |
164 | )}
165 |
166 | {isLoading && (
167 |
168 |
169 |
Processing your claim...
170 |
171 | )}
172 |
173 | {error && (
174 |
175 | {error}
176 |
177 | )}
178 |
179 | {proofData && proofData.length > 0 && (
180 |
181 |
Verification Successful
182 |
183 | {proofData.map((proof, index) => (
184 |
185 |
186 |
Proof #{index + 1}
187 | Verified
188 |
189 |
190 |
191 |
192 |
Provider
193 |
{getProviderUrl(proof)}
194 |
195 |
196 |
Timestamp
197 |
{new Date(proof.claimData.timestampS * 1000).toLocaleString()}
198 |
199 |
200 |
201 | {/* Extracted parameters section */}
202 |
203 | {renderExtractedParameters(proof)}
204 |
205 |
206 | {/* Witnesses section */}
207 | {proof.witnesses && proof.witnesses.length > 0 && (
208 |
209 |
Attested by
210 |
211 | {proof.witnesses.map((witness, widx) => (
212 |
215 | ))}
216 |
217 |
218 | )}
219 |
220 | {/* Signatures section */}
221 | {proof.signatures && proof.signatures.length > 0 && (
222 |
223 |
Signatures
224 |
225 | {proof.signatures.map((signature, sidx) => (
226 |
229 | ))}
230 |
231 |
232 | )}
233 |
234 | {/* Identifier (full) */}
235 |
236 |
Proof Identifier
237 |
238 | {proof.identifier}
239 |
240 |
241 |
242 | ))}
243 |
244 |
250 |
251 | )}
252 |
253 |
254 | )
255 | }
256 |
--------------------------------------------------------------------------------
/example/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@reclaimprotocol/js-sdk",
3 | "version": "4.2.0",
4 | "description": "Designed to request proofs from the Reclaim protocol and manage the flow of claims and witness interactions.",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "keywords": [
8 | "reclaim",
9 | "protocol",
10 | "blockchain",
11 | "proof",
12 | "verification",
13 | "identity",
14 | "claims",
15 | "witness",
16 | "sdk",
17 | "javascript",
18 | "typescript",
19 | "decentralized",
20 | "web3"
21 | ],
22 | "files": [
23 | "dist"
24 | ],
25 | "tsup": {
26 | "entry": [
27 | "src/index.ts"
28 | ],
29 | "splitting": false,
30 | "sourcemap": true,
31 | "clean": true
32 | },
33 | "scripts": {
34 | "build": "sh scripts/build.sh",
35 | "release": "release-it",
36 | "test": "echo \"Error: no test specified\" && exit 1",
37 | "commitlint": "commitlint --edit"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "https://github.com/reclaimprotocol/reclaim-js-sdk"
42 | },
43 | "author": "ali ",
44 | "license": "See License in ",
45 | "bugs": {
46 | "url": "https://github.com/reclaimprotocol/reclaim-js-sdk/issues"
47 | },
48 | "homepage": "https://github.com/reclaimprotocol/reclaim-js-sdk/",
49 | "publishConfig": {
50 | "registry": "https://registry.npmjs.org/",
51 | "access": "public"
52 | },
53 | "release-it": {
54 | "git": {
55 | "commitMessage": "chore: release ${version}",
56 | "tagName": "v${version}"
57 | },
58 | "npm": {
59 | "publish": true,
60 | "tag": "latest"
61 | },
62 | "github": {
63 | "release": true
64 | },
65 | "plugins": {
66 | "@release-it/conventional-changelog": {
67 | "preset": "angular"
68 | }
69 | }
70 | },
71 | "devDependencies": {
72 | "@bconnorwhite/bob": "^2.9.5",
73 | "@commitlint/cli": "^17.7.1",
74 | "@commitlint/config-conventional": "^17.7.0",
75 | "@release-it/conventional-changelog": "^5.0.0",
76 | "@types/qs": "^6.9.11",
77 | "@types/url-parse": "^1.4.11",
78 | "@types/uuid": "^9.0.7",
79 | "release-it": "^15.0.0",
80 | "tsup": "^8.0.1",
81 | "typescript": "^5.3.3"
82 | },
83 | "dependencies": {
84 | "canonicalize": "^2.0.0",
85 | "ethers": "^6.9.1",
86 | "qs": "^6.11.2",
87 | "url-parse": "^1.5.10",
88 | "uuid": "^9.0.1"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/readme/depemdency-diagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/readme/usage-flow-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/readme/usage-flow-3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/readme/usage-flow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Delete existing dist folder
4 | echo "Deleting existing dist folder"
5 | rm -rf dist
6 |
7 | # Build
8 | echo "Building the project"
9 | tsup --dts
10 |
--------------------------------------------------------------------------------
/src/Reclaim.ts:
--------------------------------------------------------------------------------
1 | import { type Proof, type Context, RECLAIM_EXTENSION_ACTIONS, ExtensionMessage } from './utils/interfaces'
2 | import { getIdentifierFromClaimInfo } from './witness'
3 | import {
4 | SignedClaim,
5 | ProofRequestOptions,
6 | StartSessionParams,
7 | ProofPropertiesJSON,
8 | TemplateData,
9 | InitSessionResponse,
10 | ClaimCreationType,
11 | ModalOptions,
12 | } from './utils/types'
13 | import { SessionStatus, DeviceType } from './utils/types'
14 | import { ethers } from 'ethers'
15 | import canonicalize from 'canonicalize'
16 | import {
17 | replaceAll,
18 | scheduleIntervalEndingTask
19 | } from './utils/helper'
20 | import { constants, setBackendBaseUrl } from './utils/constants'
21 | import {
22 | AddContextError,
23 | GetAppCallbackUrlError,
24 | GetStatusUrlError,
25 | InitError,
26 | InvalidParamError,
27 | ProofNotVerifiedError,
28 | ProofSubmissionFailedError,
29 | ProviderFailedError,
30 | SessionNotStartedError,
31 | SetParamsError,
32 | SetSignatureError,
33 | SignatureGeneratingError,
34 | SignatureNotFoundError
35 | } from './utils/errors'
36 | import { validateContext, validateFunctionParams, validateParameters, validateSignature, validateURL } from './utils/validationUtils'
37 | import { fetchStatusUrl, initSession, updateSession } from './utils/sessionUtils'
38 | import { assertValidSignedClaim, createLinkWithTemplateData, getWitnessesForClaim } from './utils/proofUtils'
39 | import { QRCodeModal } from './utils/modalUtils'
40 | import loggerModule from './utils/logger';
41 | import { getDeviceType, getMobileDeviceType, isMobileDevice } from './utils/device'
42 | const logger = loggerModule.logger
43 |
44 | const sdkVersion = require('../package.json').version;
45 |
46 | // Implementation
47 | export async function verifyProof(proofOrProofs: Proof | Proof[]): Promise {
48 | // If input is an array of proofs
49 | if (Array.isArray(proofOrProofs)) {
50 | for (const proof of proofOrProofs) {
51 | const isVerified = await verifyProof(proof);
52 | if (!isVerified) {
53 | return false;
54 | }
55 | }
56 | return true;
57 | }
58 |
59 | // Single proof verification logic
60 | const proof = proofOrProofs;
61 | if (!proof.signatures.length) {
62 | throw new SignatureNotFoundError('No signatures')
63 | }
64 |
65 | try {
66 | // check if witness array exist and first element is manual-verify
67 | let witnesses = []
68 | if (proof.witnesses.length && proof.witnesses[0]?.url === 'manual-verify') {
69 | witnesses.push(proof.witnesses[0].id)
70 | } else {
71 | witnesses = await getWitnessesForClaim(
72 | proof.claimData.epoch,
73 | proof.identifier,
74 | proof.claimData.timestampS
75 | )
76 | }
77 | // then hash the claim info with the encoded ctx to get the identifier
78 | const calculatedIdentifier = getIdentifierFromClaimInfo({
79 | parameters: JSON.parse(
80 | canonicalize(proof.claimData.parameters) as string
81 | ),
82 | provider: proof.claimData.provider,
83 | context: proof.claimData.context
84 | })
85 | proof.identifier = replaceAll(proof.identifier, '"', '')
86 | // check if the identifier matches the one in the proof
87 | if (calculatedIdentifier !== proof.identifier) {
88 | throw new ProofNotVerifiedError('Identifier Mismatch')
89 | }
90 |
91 | const signedClaim: SignedClaim = {
92 | claim: {
93 | ...proof.claimData
94 | },
95 | signatures: proof.signatures.map(signature => {
96 | return ethers.getBytes(signature)
97 | })
98 | }
99 |
100 | assertValidSignedClaim(signedClaim, witnesses)
101 | } catch (e: Error | unknown) {
102 | logger.info(`Error verifying proof: ${e instanceof Error ? e.message : String(e)}`)
103 | return false
104 | }
105 |
106 | return true
107 | }
108 |
109 | export function transformForOnchain(proof: Proof): { claimInfo: any, signedClaim: any } {
110 | const claimInfoBuilder = new Map([
111 | ['context', proof.claimData.context],
112 | ['parameters', proof.claimData.parameters],
113 | ['provider', proof.claimData.provider],
114 | ]);
115 | const claimInfo = Object.fromEntries(claimInfoBuilder);
116 | const claimBuilder = new Map([
117 | ['epoch', proof.claimData.epoch],
118 | ['identifier', proof.claimData.identifier],
119 | ['owner', proof.claimData.owner],
120 | ['timestampS', proof.claimData.timestampS],
121 | ]);
122 | const signedClaim = {
123 | claim: Object.fromEntries(claimBuilder),
124 | signatures: proof.signatures,
125 | };
126 | return { claimInfo, signedClaim };
127 | }
128 |
129 | // create a empty template data object to assign to templateData
130 | const emptyTemplateData: TemplateData = {
131 | sessionId: '',
132 | providerId: '',
133 | applicationId: '',
134 | signature: '',
135 | timestamp: '',
136 | callbackUrl: '',
137 | context: '',
138 | parameters: {},
139 | redirectUrl: '',
140 | acceptAiProviders: false,
141 | sdkVersion: '',
142 | providerVersion: '',
143 | resolvedProviderVersion: '',
144 | jsonProofResponse: false
145 | }
146 | export class ReclaimProofRequest {
147 | // Private class properties
148 | private applicationId: string;
149 | private signature?: string;
150 | private appCallbackUrl?: string;
151 | private sessionId: string;
152 | private options?: ProofRequestOptions;
153 | private context: Context = { contextAddress: '0x0', contextMessage: 'sample context' };
154 | private claimCreationType?: ClaimCreationType = ClaimCreationType.STANDALONE;
155 | private providerId: string;
156 | private resolvedProviderVersion?: string;
157 | private parameters: { [key: string]: string };
158 | private redirectUrl?: string;
159 | private intervals: Map = new Map();
160 | private timeStamp: string;
161 | private sdkVersion: string;
162 | private jsonProofResponse: boolean = false;
163 | private lastFailureTime?: number;
164 | private templateData: TemplateData;
165 | private extensionID: string = "reclaim-extension";
166 | private modalOptions?: ModalOptions;
167 | private readonly FAILURE_TIMEOUT = 30000; // 30 seconds timeout, can be adjusted
168 |
169 | // Private constructor
170 | private constructor(applicationId: string, providerId: string, options?: ProofRequestOptions) {
171 | this.providerId = providerId;
172 | this.timeStamp = Date.now().toString();
173 | this.applicationId = applicationId;
174 | this.sessionId = "";
175 | // keep template data as empty object
176 | this.templateData = emptyTemplateData;
177 | this.parameters = {};
178 |
179 | if (!options) {
180 | options = {};
181 | }
182 |
183 | options.useBrowserExtension = options.useBrowserExtension ?? true;
184 |
185 | if (options?.log) {
186 | loggerModule.setLogLevel('info');
187 | } else {
188 | loggerModule.setLogLevel('silent');
189 | }
190 |
191 | if (options.useAppClip === undefined) {
192 | options.useAppClip = true;
193 | }
194 |
195 | if (options?.envUrl) {
196 | setBackendBaseUrl(options.envUrl);
197 | }
198 |
199 | if (options.extensionID) {
200 | this.extensionID = options.extensionID;
201 | }
202 |
203 | this.options = options;
204 | // Fetch sdk version from package.json
205 | this.sdkVersion = 'js-' + sdkVersion;
206 | logger.info(`Initializing client with applicationId: ${this.applicationId}`);
207 | }
208 |
209 | // Static initialization methods
210 | static async init(applicationId: string, appSecret: string, providerId: string, options?: ProofRequestOptions): Promise {
211 | try {
212 | validateFunctionParams([
213 | { paramName: 'applicationId', input: applicationId, isString: true },
214 | { paramName: 'providerId', input: providerId, isString: true },
215 | { paramName: 'appSecret', input: appSecret, isString: true }
216 | ], 'the constructor')
217 |
218 | // check if options is provided and validate each property of options
219 | if (options) {
220 | if (options.acceptAiProviders) {
221 | validateFunctionParams([
222 | { paramName: 'acceptAiProviders', input: options.acceptAiProviders }
223 | ], 'the constructor')
224 | }
225 | if (options.providerVersion) {
226 | validateFunctionParams([
227 | { paramName: 'providerVersion', input: options.providerVersion, isString: true }
228 | ], 'the constructor')
229 | }
230 | if (options.log) {
231 | validateFunctionParams([
232 | { paramName: 'log', input: options.log }
233 | ], 'the constructor')
234 | }
235 | if (options.useAppClip) {
236 | validateFunctionParams([
237 | { paramName: 'useAppClip', input: options.useAppClip }
238 | ], 'the constructor')
239 | }
240 | if (options.device) {
241 | validateFunctionParams([
242 | { paramName: 'device', input: options.device, isString: true }
243 | ], 'the constructor')
244 | }
245 | if (options.useBrowserExtension) {
246 | validateFunctionParams([
247 | { paramName: 'useBrowserExtension', input: options.useBrowserExtension }
248 | ], 'the constructor')
249 | }
250 | if (options.extensionID) {
251 | validateFunctionParams([
252 | { paramName: 'extensionID', input: options.extensionID, isString: true }
253 | ], 'the constructor')
254 | }
255 | if (options.envUrl) {
256 | validateFunctionParams([
257 | { paramName: 'envUrl', input: options.envUrl, isString: true }
258 | ], 'the constructor')
259 | }
260 |
261 | }
262 |
263 | const proofRequestInstance = new ReclaimProofRequest(applicationId, providerId, options)
264 |
265 | const signature = await proofRequestInstance.generateSignature(appSecret)
266 | proofRequestInstance.setSignature(signature)
267 |
268 | const data: InitSessionResponse = await initSession(providerId, applicationId, proofRequestInstance.timeStamp, signature, options?.providerVersion);
269 | proofRequestInstance.sessionId = data.sessionId
270 | proofRequestInstance.resolvedProviderVersion = data.resolvedProviderVersion
271 |
272 | return proofRequestInstance
273 | } catch (error) {
274 | logger.info('Failed to initialize ReclaimProofRequest', error as Error);
275 | throw new InitError('Failed to initialize ReclaimProofRequest', error as Error)
276 | }
277 | }
278 |
279 | static async fromJsonString(jsonString: string): Promise {
280 | try {
281 | const {
282 | applicationId,
283 | providerId,
284 | sessionId,
285 | context,
286 | parameters,
287 | signature,
288 | redirectUrl,
289 | timeStamp,
290 | appCallbackUrl,
291 | claimCreationType,
292 | options,
293 | sdkVersion,
294 | jsonProofResponse,
295 | resolvedProviderVersion
296 | }: ProofPropertiesJSON = JSON.parse(jsonString)
297 |
298 | validateFunctionParams([
299 | { input: applicationId, paramName: 'applicationId', isString: true },
300 | { input: providerId, paramName: 'providerId', isString: true },
301 | { input: signature, paramName: 'signature', isString: true },
302 | { input: sessionId, paramName: 'sessionId', isString: true },
303 | { input: timeStamp, paramName: 'timeStamp', isString: true },
304 | { input: sdkVersion, paramName: 'sdkVersion', isString: true },
305 | ], 'fromJsonString');
306 |
307 |
308 | if (redirectUrl) {
309 | validateURL(redirectUrl, 'fromJsonString');
310 | }
311 |
312 | if (appCallbackUrl) {
313 | validateURL(appCallbackUrl, 'fromJsonString');
314 | }
315 |
316 | if (context) {
317 | validateContext(context);
318 | }
319 |
320 | if (parameters) {
321 | validateParameters(parameters);
322 | }
323 |
324 | if (claimCreationType) {
325 | validateFunctionParams([
326 | { input: claimCreationType, paramName: 'claimCreationType' }
327 | ], 'fromJsonString');
328 | }
329 |
330 | if (jsonProofResponse !== undefined) {
331 | validateFunctionParams([
332 | { input: jsonProofResponse, paramName: 'jsonProofResponse' }
333 | ], 'fromJsonString');
334 | }
335 |
336 |
337 | if (options?.providerVersion) {
338 | validateFunctionParams([
339 | { input: options.providerVersion, paramName: 'providerVersion', isString: true }
340 | ], 'fromJsonString');
341 | }
342 |
343 | if (resolvedProviderVersion) {
344 | validateFunctionParams([
345 | { input: resolvedProviderVersion, paramName: 'resolvedProviderVersion', isString: true }
346 | ], 'fromJsonString');
347 | }
348 |
349 | const proofRequestInstance = new ReclaimProofRequest(applicationId, providerId, options);
350 | proofRequestInstance.sessionId = sessionId;
351 | proofRequestInstance.context = context;
352 | proofRequestInstance.parameters = parameters;
353 | proofRequestInstance.appCallbackUrl = appCallbackUrl
354 | proofRequestInstance.redirectUrl = redirectUrl
355 | proofRequestInstance.timeStamp = timeStamp
356 | proofRequestInstance.signature = signature
357 | proofRequestInstance.sdkVersion = sdkVersion;
358 | proofRequestInstance.resolvedProviderVersion = resolvedProviderVersion;
359 | return proofRequestInstance
360 | } catch (error) {
361 | logger.info('Failed to parse JSON string in fromJsonString:', error);
362 | throw new InvalidParamError('Invalid JSON string provided to fromJsonString');
363 | }
364 | }
365 |
366 | // Setter methods
367 | setAppCallbackUrl(url: string, jsonProofResponse?: boolean): void {
368 | validateURL(url, 'setAppCallbackUrl')
369 | this.appCallbackUrl = url
370 | this.jsonProofResponse = jsonProofResponse ?? false
371 | }
372 |
373 | setRedirectUrl(url: string): void {
374 | validateURL(url, 'setRedirectUrl');
375 | this.redirectUrl = url;
376 | }
377 |
378 | setClaimCreationType(claimCreationType: ClaimCreationType): void {
379 | this.claimCreationType = claimCreationType;
380 | }
381 |
382 | setModalOptions(options: ModalOptions): void {
383 | try {
384 | // Validate modal options
385 | if (options.title !== undefined) {
386 | validateFunctionParams([
387 | { input: options.title, paramName: 'title', isString: true }
388 | ], 'setModalOptions');
389 | }
390 |
391 | if (options.description !== undefined) {
392 | validateFunctionParams([
393 | { input: options.description, paramName: 'description', isString: true }
394 | ], 'setModalOptions');
395 | }
396 |
397 | if (options.extensionUrl !== undefined) {
398 | validateURL(options.extensionUrl, 'setModalOptions');
399 | }
400 |
401 | if (options.darkTheme !== undefined) {
402 | validateFunctionParams([
403 | { input: options.darkTheme, paramName: 'darkTheme' }
404 | ], 'setModalOptions');
405 | }
406 |
407 | this.modalOptions = { ...this.modalOptions, ...options };
408 | logger.info('Modal options set successfully');
409 | } catch (error) {
410 | logger.info('Error setting modal options:', error);
411 | throw new SetParamsError('Error setting modal options', error as Error);
412 | }
413 | }
414 |
415 | addContext(address: string, message: string): void {
416 | try {
417 | validateFunctionParams([
418 | { input: address, paramName: 'address', isString: true },
419 | { input: message, paramName: 'message', isString: true }
420 | ], 'addContext');
421 | this.context = { contextAddress: address, contextMessage: message };
422 | } catch (error) {
423 | logger.info("Error adding context", error)
424 | throw new AddContextError("Error adding context", error as Error)
425 | }
426 | }
427 |
428 | setParams(params: { [key: string]: string }): void {
429 | try {
430 | validateParameters(params);
431 | this.parameters = { ...this.parameters, ...params }
432 | } catch (error) {
433 | logger.info('Error Setting Params:', error);
434 | throw new SetParamsError("Error setting params", error as Error)
435 | }
436 | }
437 |
438 | // Getter methods
439 | getAppCallbackUrl(): string {
440 | try {
441 | validateFunctionParams([{ input: this.sessionId, paramName: 'sessionId', isString: true }], 'getAppCallbackUrl');
442 | return this.appCallbackUrl || `${constants.DEFAULT_RECLAIM_CALLBACK_URL}${this.sessionId}`
443 | } catch (error) {
444 | logger.info("Error getting app callback url", error)
445 | throw new GetAppCallbackUrlError("Error getting app callback url", error as Error)
446 | }
447 | }
448 |
449 | getStatusUrl(): string {
450 | try {
451 | validateFunctionParams([{ input: this.sessionId, paramName: 'sessionId', isString: true }], 'getStatusUrl');
452 | return `${constants.DEFAULT_RECLAIM_STATUS_URL}${this.sessionId}`
453 | } catch (error) {
454 | logger.info("Error fetching Status Url", error)
455 | throw new GetStatusUrlError("Error fetching status url", error as Error)
456 | }
457 | }
458 |
459 | // getter for SessionId
460 | getSessionId(): string {
461 | if (!this.sessionId) {
462 | throw new SessionNotStartedError("SessionId is not set");
463 | }
464 | return this.sessionId;
465 | }
466 |
467 | // Private helper methods
468 | private setSignature(signature: string): void {
469 | try {
470 | validateFunctionParams([{ input: signature, paramName: 'signature', isString: true }], 'setSignature');
471 | this.signature = signature;
472 | logger.info(`Signature set successfully for applicationId: ${this.applicationId}`);
473 | } catch (error) {
474 | logger.info("Error setting signature", error)
475 | throw new SetSignatureError("Error setting signature", error as Error)
476 | }
477 | }
478 |
479 | private async generateSignature(applicationSecret: string): Promise {
480 | try {
481 | const wallet = new ethers.Wallet(applicationSecret)
482 | const canonicalData = canonicalize({ providerId: this.providerId, timestamp: this.timeStamp });
483 |
484 |
485 | if (!canonicalData) {
486 | throw new SignatureGeneratingError('Failed to canonicalize data for signing.');
487 | }
488 |
489 | const messageHash = ethers.keccak256(new TextEncoder().encode(canonicalData));
490 |
491 | return await wallet.signMessage(ethers.getBytes(messageHash));
492 | } catch (err) {
493 | logger.info(`Error generating proof request for applicationId: ${this.applicationId}, providerId: ${this.providerId}, signature: ${this.signature}, timeStamp: ${this.timeStamp}`, err);
494 | throw new SignatureGeneratingError(`Error generating signature for applicationSecret: ${applicationSecret}`)
495 | }
496 | }
497 |
498 | private clearInterval(): void {
499 | if (this.sessionId && this.intervals.has(this.sessionId)) {
500 | clearInterval(this.intervals.get(this.sessionId) as NodeJS.Timeout)
501 | this.intervals.delete(this.sessionId)
502 | }
503 | }
504 |
505 | // Public methods
506 | toJsonString(options?: ProofRequestOptions): string {
507 | return JSON.stringify({
508 | applicationId: this.applicationId,
509 | providerId: this.providerId,
510 | sessionId: this.sessionId,
511 | context: this.context,
512 | appCallbackUrl: this.appCallbackUrl,
513 | claimCreationType: this.claimCreationType,
514 | parameters: this.parameters,
515 | signature: this.signature,
516 | redirectUrl: this.redirectUrl,
517 | timeStamp: this.timeStamp,
518 | options: this.options,
519 | sdkVersion: this.sdkVersion,
520 | jsonProofResponse: this.jsonProofResponse,
521 | resolvedProviderVersion: this.resolvedProviderVersion ?? ''
522 | })
523 | }
524 |
525 | async getRequestUrl(): Promise {
526 | logger.info('Creating Request Url')
527 | if (!this.signature) {
528 | throw new SignatureNotFoundError('Signature is not set.')
529 | }
530 |
531 | try {
532 | validateSignature(this.providerId, this.signature, this.applicationId, this.timeStamp)
533 |
534 | const templateData: TemplateData = {
535 | sessionId: this.sessionId,
536 | providerId: this.providerId,
537 | providerVersion: this.options?.providerVersion ?? '',
538 | resolvedProviderVersion: this.resolvedProviderVersion ?? '',
539 | applicationId: this.applicationId,
540 | signature: this.signature,
541 | timestamp: this.timeStamp,
542 | callbackUrl: this.getAppCallbackUrl(),
543 | context: JSON.stringify(this.context),
544 | parameters: this.parameters,
545 | redirectUrl: this.redirectUrl ?? '',
546 | acceptAiProviders: this.options?.acceptAiProviders ?? false,
547 | sdkVersion: this.sdkVersion,
548 | jsonProofResponse: this.jsonProofResponse
549 |
550 | }
551 | await updateSession(this.sessionId, SessionStatus.SESSION_STARTED)
552 | if (this.options?.useAppClip) {
553 | let template = encodeURIComponent(JSON.stringify(templateData));
554 | template = replaceAll(template, '(', '%28');
555 | template = replaceAll(template, ')', '%29');
556 |
557 | // check if the app is running on iOS or Android
558 | const isIos = getMobileDeviceType() === DeviceType.IOS;
559 | if (!isIos) {
560 | const instantAppUrl = `https://share.reclaimprotocol.org/verify/?template=${template}`;
561 | logger.info('Instant App Url created successfully: ' + instantAppUrl);
562 | return instantAppUrl;
563 | } else {
564 | const appClipUrl = `https://appclip.apple.com/id?p=org.reclaimprotocol.app.clip&template=${template}`;
565 | logger.info('App Clip Url created successfully: ' + appClipUrl);
566 | return appClipUrl;
567 | }
568 | } else {
569 | const link = await createLinkWithTemplateData(templateData)
570 | logger.info('Request Url created successfully: ' + link);
571 | return link;
572 | }
573 | } catch (error) {
574 | logger.info('Error creating Request Url:', error)
575 | throw error
576 | }
577 | }
578 |
579 | async triggerReclaimFlow(): Promise {
580 | if (!this.signature) {
581 | throw new SignatureNotFoundError('Signature is not set.')
582 | }
583 |
584 | try {
585 | validateSignature(this.providerId, this.signature, this.applicationId, this.timeStamp)
586 | const templateData: TemplateData = {
587 | sessionId: this.sessionId,
588 | providerId: this.providerId,
589 | applicationId: this.applicationId,
590 | signature: this.signature,
591 | timestamp: this.timeStamp,
592 | callbackUrl: this.getAppCallbackUrl(),
593 | context: JSON.stringify(this.context),
594 | providerVersion: this.options?.providerVersion ?? '',
595 | resolvedProviderVersion: this.resolvedProviderVersion ?? '',
596 | parameters: this.parameters,
597 | redirectUrl: this.redirectUrl ?? '',
598 | acceptAiProviders: this.options?.acceptAiProviders ?? false,
599 | sdkVersion: this.sdkVersion,
600 | jsonProofResponse: this.jsonProofResponse
601 | }
602 |
603 | this.templateData = templateData;
604 |
605 | logger.info('Triggering Reclaim flow');
606 |
607 | // Get device type
608 | const deviceType = getDeviceType();
609 | await updateSession(this.sessionId, SessionStatus.SESSION_STARTED)
610 |
611 | if (deviceType === DeviceType.DESKTOP) {
612 | const extensionAvailable = await this.isBrowserExtensionAvailable();
613 | // Desktop flow
614 | if (this.options?.useBrowserExtension && extensionAvailable) {
615 | logger.info('Triggering browser extension flow');
616 | this.triggerBrowserExtensionFlow();
617 | return;
618 | } else {
619 | // Show QR code popup modal
620 | logger.info('Browser extension not available, showing QR code modal');
621 | await this.showQRCodeModal();
622 | }
623 | } else if (deviceType === DeviceType.MOBILE) {
624 | // Mobile flow
625 | const mobileDeviceType = getMobileDeviceType();
626 |
627 | if (mobileDeviceType === DeviceType.ANDROID) {
628 | // Redirect to instant app URL
629 | logger.info('Redirecting to Android instant app');
630 | await this.redirectToInstantApp();
631 | } else if (mobileDeviceType === DeviceType.IOS) {
632 | // Redirect to app clip URL
633 | logger.info('Redirecting to iOS app clip');
634 | await this.redirectToAppClip();
635 | }
636 | }
637 | } catch (error) {
638 | logger.info('Error triggering Reclaim flow:', error);
639 | throw error;
640 | }
641 | }
642 |
643 |
644 | async isBrowserExtensionAvailable(timeout = 200): Promise {
645 | try {
646 | return new Promise((resolve) => {
647 | const messageId = `reclaim-check-${Date.now()}`;
648 |
649 | const timeoutId = setTimeout(() => {
650 | window.removeEventListener('message', messageListener);
651 | resolve(false);
652 | }, timeout);
653 |
654 | const messageListener = (event: MessageEvent) => {
655 | if (event.data?.action === RECLAIM_EXTENSION_ACTIONS.EXTENSION_RESPONSE &&
656 | event.data?.messageId === messageId) {
657 | clearTimeout(timeoutId);
658 | window.removeEventListener('message', messageListener);
659 | resolve(!!event.data.installed);
660 | }
661 | };
662 |
663 | window.addEventListener('message', messageListener);
664 | const message: ExtensionMessage = {
665 | action: RECLAIM_EXTENSION_ACTIONS.CHECK_EXTENSION,
666 | extensionID: this.extensionID,
667 | messageId: messageId
668 | }
669 | window.postMessage(message, '*');
670 | });
671 | } catch (error) {
672 | logger.info('Error checking Reclaim extension installed:', error);
673 | return false;
674 | }
675 | }
676 |
677 | private triggerBrowserExtensionFlow(): void {
678 | const message: ExtensionMessage = {
679 | action: RECLAIM_EXTENSION_ACTIONS.START_VERIFICATION,
680 | messageId: this.sessionId,
681 | data: this.templateData,
682 | extensionID: this.extensionID
683 | }
684 | window.postMessage(message, '*');
685 | logger.info('Browser extension flow triggered');
686 | }
687 |
688 | private async showQRCodeModal(): Promise {
689 | try {
690 | const requestUrl = await createLinkWithTemplateData(this.templateData);
691 | const modal = new QRCodeModal(this.modalOptions);
692 | await modal.show(requestUrl);
693 | } catch (error) {
694 | logger.info('Error showing QR code modal:', error);
695 | throw error;
696 | }
697 | }
698 |
699 | private async redirectToInstantApp(): Promise {
700 | try {
701 | let template = encodeURIComponent(JSON.stringify(this.templateData));
702 | template = replaceAll(template, '(', '%28');
703 | template = replaceAll(template, ')', '%29');
704 |
705 | const instantAppUrl = `https://share.reclaimprotocol.org/verify/?template=${template}`;
706 | logger.info('Redirecting to Android instant app: ' + instantAppUrl);
707 |
708 | // Redirect to instant app
709 | window.location.href = instantAppUrl;
710 | } catch (error) {
711 | logger.info('Error redirecting to instant app:', error);
712 | throw error;
713 | }
714 | }
715 |
716 | private async redirectToAppClip(): Promise {
717 | try {
718 | let template = encodeURIComponent(JSON.stringify(this.templateData));
719 | template = replaceAll(template, '(', '%28');
720 | template = replaceAll(template, ')', '%29');
721 |
722 | const appClipUrl = `https://appclip.apple.com/id?p=org.reclaimprotocol.app.clip&template=${template}`;
723 | logger.info('Redirecting to iOS app clip: ' + appClipUrl);
724 |
725 | // Redirect to app clip
726 | window.location.href = appClipUrl;
727 | } catch (error) {
728 | logger.info('Error redirecting to app clip:', error);
729 | throw error;
730 | }
731 | }
732 |
733 | async startSession({ onSuccess, onError }: StartSessionParams): Promise {
734 | if (!this.sessionId) {
735 | const message = "Session can't be started due to undefined value of sessionId";
736 | logger.info(message);
737 | throw new SessionNotStartedError(message);
738 | }
739 |
740 | logger.info('Starting session');
741 | const interval = setInterval(async () => {
742 | try {
743 | const statusUrlResponse = await fetchStatusUrl(this.sessionId);
744 |
745 | if (!statusUrlResponse.session) return;
746 |
747 | // Reset failure time if status is not PROOF_GENERATION_FAILED
748 | if (statusUrlResponse.session.statusV2 !== SessionStatus.PROOF_GENERATION_FAILED) {
749 | this.lastFailureTime = undefined;
750 | }
751 |
752 | // Check for failure timeout
753 | if (statusUrlResponse.session.statusV2 === SessionStatus.PROOF_GENERATION_FAILED) {
754 | const currentTime = Date.now();
755 | if (!this.lastFailureTime) {
756 | this.lastFailureTime = currentTime;
757 | } else if (currentTime - this.lastFailureTime >= this.FAILURE_TIMEOUT) {
758 | throw new ProviderFailedError('Proof generation failed - timeout reached');
759 | }
760 | return; // Continue monitoring if under timeout
761 | }
762 |
763 | const isDefaultCallbackUrl = this.getAppCallbackUrl() === `${constants.DEFAULT_RECLAIM_CALLBACK_URL}${this.sessionId}`;
764 |
765 | if (isDefaultCallbackUrl) {
766 | if (statusUrlResponse.session.proofs && statusUrlResponse.session.proofs.length > 0) {
767 | const proofs = statusUrlResponse.session.proofs;
768 | if (this.claimCreationType === ClaimCreationType.STANDALONE) {
769 | const verified = await verifyProof(proofs);
770 | if (!verified) {
771 | logger.info(`Proofs not verified: ${JSON.stringify(proofs)}`);
772 | throw new ProofNotVerifiedError();
773 | }
774 | }
775 | // check if the proofs array has only one proof then send the proofs in onSuccess
776 | if (proofs.length === 1) {
777 | onSuccess(proofs[0]);
778 | } else {
779 | onSuccess(proofs);
780 | }
781 | this.clearInterval();
782 | }
783 | } else {
784 | if (statusUrlResponse.session.statusV2 === SessionStatus.PROOF_SUBMISSION_FAILED) {
785 | throw new ProofSubmissionFailedError();
786 | }
787 | if (statusUrlResponse.session.statusV2 === SessionStatus.PROOF_SUBMITTED) {
788 | if (onSuccess) {
789 | onSuccess('Proof submitted successfully to the custom callback url');
790 | }
791 | this.clearInterval();
792 | }
793 | }
794 | } catch (e) {
795 | if (onError) {
796 | onError(e as Error);
797 | }
798 | this.clearInterval();
799 | }
800 | }, 3000);
801 |
802 | this.intervals.set(this.sessionId, interval);
803 | scheduleIntervalEndingTask(this.sessionId, this.intervals, onError);
804 | }
805 | }
806 |
807 |
--------------------------------------------------------------------------------
/src/contract-types/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "0x1a4": {
3 | "chainName": "opt-goerli",
4 | "address": "0xF93F605142Fb1Efad7Aa58253dDffF67775b4520",
5 | "rpcUrl": "https://opt-goerli.g.alchemy.com/v2/rksDkSUXd2dyk2ANy_zzODknx_AAokui"
6 | },
7 | "0xaa37dc": {
8 | "chainName": "opt-sepolia",
9 | "address": "0x6D0f81BDA11995f25921aAd5B43359630E65Ca96",
10 | "rpcUrl": "https://opt-sepolia.g.alchemy.com/v2/aO1-SfG4oFRLyAiLREqzyAUu0HTCwHgs"
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/contract-types/contracts/factories/Reclaim__factory.ts:
--------------------------------------------------------------------------------
1 | /* Autogenerated file. Do not edit manually. */
2 | /* tslint:disable */
3 |
4 | import { Contract } from 'ethers';
5 |
6 | const _abi = [
7 | {
8 | anonymous: false,
9 | inputs: [
10 | {
11 | indexed: false,
12 | internalType: 'address',
13 | name: 'previousAdmin',
14 | type: 'address',
15 | },
16 | {
17 | indexed: false,
18 | internalType: 'address',
19 | name: 'newAdmin',
20 | type: 'address',
21 | },
22 | ],
23 | name: 'AdminChanged',
24 | type: 'event',
25 | },
26 | {
27 | anonymous: false,
28 | inputs: [
29 | {
30 | indexed: true,
31 | internalType: 'address',
32 | name: 'beacon',
33 | type: 'address',
34 | },
35 | ],
36 | name: 'BeaconUpgraded',
37 | type: 'event',
38 | },
39 | {
40 | anonymous: false,
41 | inputs: [
42 | {
43 | components: [
44 | {
45 | internalType: 'uint32',
46 | name: 'id',
47 | type: 'uint32',
48 | },
49 | {
50 | internalType: 'uint32',
51 | name: 'timestampStart',
52 | type: 'uint32',
53 | },
54 | {
55 | internalType: 'uint32',
56 | name: 'timestampEnd',
57 | type: 'uint32',
58 | },
59 | {
60 | components: [
61 | {
62 | internalType: 'address',
63 | name: 'addr',
64 | type: 'address',
65 | },
66 | {
67 | internalType: 'string',
68 | name: 'host',
69 | type: 'string',
70 | },
71 | ],
72 | internalType: 'struct Reclaim.Witness[]',
73 | name: 'witnesses',
74 | type: 'tuple[]',
75 | },
76 | {
77 | internalType: 'uint8',
78 | name: 'minimumWitnessesForClaimCreation',
79 | type: 'uint8',
80 | },
81 | ],
82 | indexed: false,
83 | internalType: 'struct Reclaim.Epoch',
84 | name: 'epoch',
85 | type: 'tuple',
86 | },
87 | ],
88 | name: 'EpochAdded',
89 | type: 'event',
90 | },
91 | {
92 | anonymous: false,
93 | inputs: [
94 | {
95 | indexed: false,
96 | internalType: 'uint8',
97 | name: 'version',
98 | type: 'uint8',
99 | },
100 | ],
101 | name: 'Initialized',
102 | type: 'event',
103 | },
104 | {
105 | anonymous: false,
106 | inputs: [
107 | {
108 | indexed: true,
109 | internalType: 'address',
110 | name: 'previousOwner',
111 | type: 'address',
112 | },
113 | {
114 | indexed: true,
115 | internalType: 'address',
116 | name: 'newOwner',
117 | type: 'address',
118 | },
119 | ],
120 | name: 'OwnershipTransferred',
121 | type: 'event',
122 | },
123 | {
124 | anonymous: false,
125 | inputs: [
126 | {
127 | indexed: true,
128 | internalType: 'address',
129 | name: 'implementation',
130 | type: 'address',
131 | },
132 | ],
133 | name: 'Upgraded',
134 | type: 'event',
135 | },
136 | {
137 | inputs: [
138 | {
139 | internalType: 'address',
140 | name: 'witnessAddress',
141 | type: 'address',
142 | },
143 | {
144 | internalType: 'string',
145 | name: 'host',
146 | type: 'string',
147 | },
148 | ],
149 | name: 'addAsWitness',
150 | outputs: [],
151 | stateMutability: 'nonpayable',
152 | type: 'function',
153 | },
154 | {
155 | inputs: [],
156 | name: 'addNewEpoch',
157 | outputs: [],
158 | stateMutability: 'nonpayable',
159 | type: 'function',
160 | },
161 | {
162 | inputs: [
163 | {
164 | internalType: 'uint32',
165 | name: 'epochNum',
166 | type: 'uint32',
167 | },
168 | {
169 | components: [
170 | {
171 | internalType: 'string',
172 | name: 'provider',
173 | type: 'string',
174 | },
175 | {
176 | internalType: 'string',
177 | name: 'parameters',
178 | type: 'string',
179 | },
180 | {
181 | internalType: 'string',
182 | name: 'context',
183 | type: 'string',
184 | },
185 | ],
186 | internalType: 'struct Claims.ClaimInfo',
187 | name: 'claimInfo',
188 | type: 'tuple',
189 | },
190 | {
191 | components: [
192 | {
193 | internalType: 'bytes32',
194 | name: 'identifier',
195 | type: 'bytes32',
196 | },
197 | {
198 | internalType: 'address',
199 | name: 'owner',
200 | type: 'address',
201 | },
202 | {
203 | internalType: 'uint32',
204 | name: 'timestampS',
205 | type: 'uint32',
206 | },
207 | {
208 | internalType: 'uint256',
209 | name: 'epoch',
210 | type: 'uint256',
211 | },
212 | ],
213 | internalType: 'struct Claims.CompleteClaimData',
214 | name: 'claimData',
215 | type: 'tuple',
216 | },
217 | {
218 | internalType: 'bytes[]',
219 | name: 'signatures',
220 | type: 'bytes[]',
221 | },
222 | ],
223 | name: 'assertValidEpochAndSignedClaim',
224 | outputs: [],
225 | stateMutability: 'view',
226 | type: 'function',
227 | },
228 | {
229 | inputs: [],
230 | name: 'currentEpoch',
231 | outputs: [
232 | {
233 | internalType: 'uint32',
234 | name: '',
235 | type: 'uint32',
236 | },
237 | ],
238 | stateMutability: 'view',
239 | type: 'function',
240 | },
241 | {
242 | inputs: [],
243 | name: 'epochDurationS',
244 | outputs: [
245 | {
246 | internalType: 'uint32',
247 | name: '',
248 | type: 'uint32',
249 | },
250 | ],
251 | stateMutability: 'view',
252 | type: 'function',
253 | },
254 | {
255 | inputs: [
256 | {
257 | internalType: 'uint256',
258 | name: '',
259 | type: 'uint256',
260 | },
261 | ],
262 | name: 'epochs',
263 | outputs: [
264 | {
265 | internalType: 'uint32',
266 | name: 'id',
267 | type: 'uint32',
268 | },
269 | {
270 | internalType: 'uint32',
271 | name: 'timestampStart',
272 | type: 'uint32',
273 | },
274 | {
275 | internalType: 'uint32',
276 | name: 'timestampEnd',
277 | type: 'uint32',
278 | },
279 | {
280 | internalType: 'uint8',
281 | name: 'minimumWitnessesForClaimCreation',
282 | type: 'uint8',
283 | },
284 | ],
285 | stateMutability: 'view',
286 | type: 'function',
287 | },
288 | {
289 | inputs: [
290 | {
291 | internalType: 'uint32',
292 | name: 'epoch',
293 | type: 'uint32',
294 | },
295 | ],
296 | name: 'fetchEpoch',
297 | outputs: [
298 | {
299 | components: [
300 | {
301 | internalType: 'uint32',
302 | name: 'id',
303 | type: 'uint32',
304 | },
305 | {
306 | internalType: 'uint32',
307 | name: 'timestampStart',
308 | type: 'uint32',
309 | },
310 | {
311 | internalType: 'uint32',
312 | name: 'timestampEnd',
313 | type: 'uint32',
314 | },
315 | {
316 | components: [
317 | {
318 | internalType: 'address',
319 | name: 'addr',
320 | type: 'address',
321 | },
322 | {
323 | internalType: 'string',
324 | name: 'host',
325 | type: 'string',
326 | },
327 | ],
328 | internalType: 'struct Reclaim.Witness[]',
329 | name: 'witnesses',
330 | type: 'tuple[]',
331 | },
332 | {
333 | internalType: 'uint8',
334 | name: 'minimumWitnessesForClaimCreation',
335 | type: 'uint8',
336 | },
337 | ],
338 | internalType: 'struct Reclaim.Epoch',
339 | name: '',
340 | type: 'tuple',
341 | },
342 | ],
343 | stateMutability: 'view',
344 | type: 'function',
345 | },
346 | {
347 | inputs: [
348 | {
349 | internalType: 'uint32',
350 | name: 'epoch',
351 | type: 'uint32',
352 | },
353 | {
354 | internalType: 'bytes32',
355 | name: 'identifier',
356 | type: 'bytes32',
357 | },
358 | {
359 | internalType: 'uint32',
360 | name: 'timestampS',
361 | type: 'uint32',
362 | },
363 | ],
364 | name: 'fetchWitnessesForClaim',
365 | outputs: [
366 | {
367 | components: [
368 | {
369 | internalType: 'address',
370 | name: 'addr',
371 | type: 'address',
372 | },
373 | {
374 | internalType: 'string',
375 | name: 'host',
376 | type: 'string',
377 | },
378 | ],
379 | internalType: 'struct Reclaim.Witness[]',
380 | name: '',
381 | type: 'tuple[]',
382 | },
383 | ],
384 | stateMutability: 'view',
385 | type: 'function',
386 | },
387 | {
388 | inputs: [],
389 | name: 'initialize',
390 | outputs: [],
391 | stateMutability: 'nonpayable',
392 | type: 'function',
393 | },
394 | {
395 | inputs: [],
396 | name: 'minimumWitnessesForClaimCreation',
397 | outputs: [
398 | {
399 | internalType: 'uint8',
400 | name: '',
401 | type: 'uint8',
402 | },
403 | ],
404 | stateMutability: 'view',
405 | type: 'function',
406 | },
407 | {
408 | inputs: [],
409 | name: 'owner',
410 | outputs: [
411 | {
412 | internalType: 'address',
413 | name: '',
414 | type: 'address',
415 | },
416 | ],
417 | stateMutability: 'view',
418 | type: 'function',
419 | },
420 | {
421 | inputs: [],
422 | name: 'proxiableUUID',
423 | outputs: [
424 | {
425 | internalType: 'bytes32',
426 | name: '',
427 | type: 'bytes32',
428 | },
429 | ],
430 | stateMutability: 'view',
431 | type: 'function',
432 | },
433 | {
434 | inputs: [
435 | {
436 | internalType: 'address',
437 | name: 'witnessAddress',
438 | type: 'address',
439 | },
440 | ],
441 | name: 'removeAsWitness',
442 | outputs: [],
443 | stateMutability: 'nonpayable',
444 | type: 'function',
445 | },
446 | {
447 | inputs: [],
448 | name: 'renounceOwnership',
449 | outputs: [],
450 | stateMutability: 'nonpayable',
451 | type: 'function',
452 | },
453 | {
454 | inputs: [
455 | {
456 | internalType: 'address',
457 | name: 'newOwner',
458 | type: 'address',
459 | },
460 | ],
461 | name: 'transferOwnership',
462 | outputs: [],
463 | stateMutability: 'nonpayable',
464 | type: 'function',
465 | },
466 | {
467 | inputs: [
468 | {
469 | internalType: 'address',
470 | name: 'addr',
471 | type: 'address',
472 | },
473 | {
474 | internalType: 'bool',
475 | name: 'isWhitelisted',
476 | type: 'bool',
477 | },
478 | ],
479 | name: 'updateWitnessWhitelist',
480 | outputs: [],
481 | stateMutability: 'nonpayable',
482 | type: 'function',
483 | },
484 | {
485 | inputs: [
486 | {
487 | internalType: 'address',
488 | name: 'newImplementation',
489 | type: 'address',
490 | },
491 | ],
492 | name: 'upgradeTo',
493 | outputs: [],
494 | stateMutability: 'nonpayable',
495 | type: 'function',
496 | },
497 | {
498 | inputs: [
499 | {
500 | internalType: 'address',
501 | name: 'newImplementation',
502 | type: 'address',
503 | },
504 | {
505 | internalType: 'bytes',
506 | name: 'data',
507 | type: 'bytes',
508 | },
509 | ],
510 | name: 'upgradeToAndCall',
511 | outputs: [],
512 | stateMutability: 'payable',
513 | type: 'function',
514 | },
515 | {
516 | inputs: [
517 | {
518 | internalType: 'uint256',
519 | name: '',
520 | type: 'uint256',
521 | },
522 | ],
523 | name: 'witnesses',
524 | outputs: [
525 | {
526 | internalType: 'address',
527 | name: 'addr',
528 | type: 'address',
529 | },
530 | {
531 | internalType: 'string',
532 | name: 'host',
533 | type: 'string',
534 | },
535 | ],
536 | stateMutability: 'view',
537 | type: 'function',
538 | },
539 | ] as const;
540 |
541 | export class Reclaim__factory {
542 | static readonly abi = _abi;
543 |
544 | static connect(address: string, signerOrProvider: any): Contract {
545 | return new Contract(address, _abi, signerOrProvider);
546 | }
547 | }
548 |
--------------------------------------------------------------------------------
/src/contract-types/contracts/factories/index.ts:
--------------------------------------------------------------------------------
1 | export { Reclaim__factory } from './Reclaim__factory';
2 |
--------------------------------------------------------------------------------
/src/contract-types/contracts/index.ts:
--------------------------------------------------------------------------------
1 | /* Autogenerated file. Do not edit manually. */
2 | /* tslint:disable */
3 | export { Reclaim__factory } from './factories/Reclaim__factory';
4 |
--------------------------------------------------------------------------------
/src/contract-types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './contracts';
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Reclaim';
2 | export * from './utils/interfaces';
3 | export { ClaimCreationType, ModalOptions } from './utils/types';
--------------------------------------------------------------------------------
/src/smart-contract.ts:
--------------------------------------------------------------------------------
1 | import type { Beacon, BeaconState } from './utils/interfaces';
2 | import { Reclaim__factory as ReclaimFactory } from './contract-types';
3 | import CONTRACTS_CONFIG from './contract-types/config.json';
4 | import { Contract, ethers } from 'ethers';
5 |
6 | const DEFAULT_CHAIN_ID = 11155420;
7 |
8 | export function makeBeacon(chainId?: number): Beacon | undefined {
9 | chainId = chainId || DEFAULT_CHAIN_ID;
10 | const contract = getContract(chainId);
11 | if (contract) {
12 | return makeBeaconCacheable({
13 | async getState(epochId: number | undefined): Promise {
14 | //@ts-ignore
15 | const epoch = await contract.fetchEpoch(epochId || 0);
16 | if (!epoch.id) {
17 | throw new Error(`Invalid epoch ID: ${epochId}`);
18 | }
19 |
20 | return {
21 | epoch: epoch.id,
22 | witnesses: epoch.witnesses.map((w: any) => ({
23 | id: w.addr.toLowerCase(),
24 | url: w.host,
25 | })),
26 | witnessesRequiredForClaim: epoch.minimumWitnessesForClaimCreation,
27 | nextEpochTimestampS: epoch.timestampEnd,
28 | };
29 | },
30 | });
31 | } else {
32 | return undefined;
33 | }
34 | }
35 |
36 | export function makeBeaconCacheable(beacon: Beacon): Beacon {
37 | const cache: { [epochId: number]: Promise } = {};
38 |
39 | return {
40 | ...beacon,
41 | async getState(epochId: number | undefined): Promise {
42 | if (!epochId) {
43 | // TODO: add cache here
44 | const state = await beacon.getState();
45 | return state;
46 | }
47 |
48 | const key = epochId;
49 |
50 | if (!cache[key]) {
51 | cache[key] = beacon.getState(epochId);
52 | }
53 |
54 | return cache[key] as unknown as BeaconState;
55 | },
56 | };
57 | }
58 |
59 | const existingContractsMap: { [chain: string]: Contract } = {};
60 |
61 | function getContract(chainId: number): Contract {
62 | const chainKey = `0x${chainId.toString(16)}`;
63 | if (!existingContractsMap[chainKey]) {
64 | const contractData =
65 | CONTRACTS_CONFIG[chainKey as keyof typeof CONTRACTS_CONFIG];
66 | if (!contractData) {
67 | throw new Error(`Unsupported chain: "${chainKey}"`);
68 | }
69 |
70 | const rpcProvider = new ethers.JsonRpcProvider(contractData.rpcUrl);
71 | existingContractsMap[chainKey] = ReclaimFactory.connect(
72 | contractData.address,
73 | rpcProvider
74 | );
75 | }
76 |
77 | return existingContractsMap[chainKey] as Contract;
78 | }
79 |
--------------------------------------------------------------------------------
/src/utils/__tests__/helper.test.ts:
--------------------------------------------------------------------------------
1 | import { escapeRegExp, replaceAll } from '../helper';
2 |
3 | describe('escapeRegExp', () => {
4 | it('should escape special characters in a string', () => {
5 | const input = '.*+?^${}()|[]\\';
6 | const expectedOutput = '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\';
7 | expect(escapeRegExp(input)).toBe(expectedOutput);
8 | });
9 |
10 | it('should return the same string if there are no special characters', () => {
11 | const input = 'abc123';
12 | const expectedOutput = 'abc123';
13 | expect(escapeRegExp(input)).toBe(expectedOutput);
14 | });
15 |
16 | it('should return an empty string if the input is empty', () => {
17 | const input = '';
18 | const expectedOutput = '';
19 | expect(escapeRegExp(input)).toBe(expectedOutput);
20 | });
21 | });
22 |
23 | describe('replaceAll', () => {
24 | it('should replace all occurrences of a substring in a string', () => {
25 | const str = 'hello world, hello universe';
26 | const find = 'hello';
27 | const replace = 'hi';
28 | const expectedOutput = 'hi world, hi universe';
29 | expect(replaceAll(str, find, replace)).toBe(expectedOutput);
30 | });
31 |
32 | it('should return the same string if the substring to find is not present', () => {
33 | const str = 'hello world';
34 | const find = 'bye';
35 | const replace = 'hi';
36 | const expectedOutput = 'hello world';
37 | expect(replaceAll(str, find, replace)).toBe(expectedOutput);
38 | });
39 |
40 | it('should return the same string if the substring to find is empty', () => {
41 | const str = 'hello world';
42 | const find = '';
43 | const replace = 'hi';
44 | const expectedOutput = 'hello world';
45 | expect(replaceAll(str, find, replace)).toBe(expectedOutput);
46 | });
47 |
48 | it('should replace all occurrences of a substring containing special regex characters', () => {
49 | const str = 'hello.world.hello.world';
50 | const find = '.';
51 | const replace = '!';
52 | const expectedOutput = 'hello!world!hello!world';
53 | expect(replaceAll(str, find, replace)).toBe(expectedOutput);
54 | });
55 |
56 | it('should return an empty string if the input string is empty', () => {
57 | const str = '';
58 | const find = 'hello';
59 | const replace = 'hi';
60 | const expectedOutput = '';
61 | expect(replaceAll(str, find, replace)).toBe(expectedOutput);
62 | });
63 |
64 | it('should handle overlapping substrings correctly', () => {
65 | const str = 'aaa';
66 | const find = 'aa';
67 | const replace = 'b';
68 | const expectedOutput = 'ba';
69 | expect(replaceAll(str, find, replace)).toBe(expectedOutput);
70 | });
71 |
72 | it('should replace the substring with an empty string', () => {
73 | const str = 'hello world';
74 | const find = 'world';
75 | const replace = '';
76 | const expectedOutput = 'hello ';
77 | expect(replaceAll(str, find, replace)).toBe(expectedOutput);
78 | });
79 | });
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | // Base URL for the backend API
2 | export let BACKEND_BASE_URL = "https://api.reclaimprotocol.org";
3 |
4 | export function setBackendBaseUrl(url: string) {
5 | BACKEND_BASE_URL = url;
6 | }
7 |
8 | // Constant values used throughout the application
9 | export const constants = {
10 | // Default callback URL for Reclaim protocol
11 | get DEFAULT_RECLAIM_CALLBACK_URL() {
12 | return `${BACKEND_BASE_URL}/api/sdk/callback?callbackId=`;
13 | },
14 |
15 | // Default status URL for Reclaim sessions
16 | get DEFAULT_RECLAIM_STATUS_URL() {
17 | return `${BACKEND_BASE_URL}/api/sdk/session/`;
18 | },
19 |
20 | // URL for sharing Reclaim templates
21 | RECLAIM_SHARE_URL: 'https://share.reclaimprotocol.org/verifier/?template=',
22 |
23 | // Chrome extension URL for Reclaim Protocol
24 | CHROME_EXTENSION_URL: 'https://chromewebstore.google.com/',
25 |
26 | // QR Code API base URL
27 | QR_CODE_API_URL: 'https://api.qrserver.com/v1/create-qr-code/'
28 | };
29 |
--------------------------------------------------------------------------------
/src/utils/device.ts:
--------------------------------------------------------------------------------
1 | import { DeviceType } from "./types";
2 |
3 | const navigatorDefined = typeof navigator !== 'undefined';
4 | const windowDefined = typeof window !== 'undefined';
5 |
6 | const userAgent = navigatorDefined ? navigator.userAgent.toLowerCase() : '';
7 | const userAgentData = navigatorDefined ? (navigator as Navigator & {
8 | userAgentData?: {
9 | platform: string;
10 | brands?: { brand: string; version: string }[];
11 | }
12 | }).userAgentData : undefined;
13 |
14 | /**
15 | * Highly accurate device type detection - returns only 'desktop' or 'mobile'
16 | * Uses multiple detection methods and scoring system for maximum accuracy
17 | * @returns {DeviceType.DESKTOP | DeviceType.MOBILE} The detected device type
18 | */
19 | export function getDeviceType(): DeviceType.DESKTOP | DeviceType.MOBILE {
20 | // Early return for server-side rendering - assume desktop
21 | if (!navigatorDefined || !windowDefined) {
22 | return DeviceType.DESKTOP;
23 | }
24 |
25 | let mobileScore = 0;
26 | const CONFIDENCE_THRESHOLD = 3; // Need at least 3 indicators for mobile
27 |
28 | // Method 1: Touch capability detection (weight: 2)
29 | const isTouchDevice = 'ontouchstart' in window ||
30 | (navigatorDefined && navigator.maxTouchPoints > 0);
31 | if (isTouchDevice) {
32 | mobileScore += 2;
33 | }
34 |
35 | // Method 2: Screen size analysis (weight: 2)
36 | const screenWidth = window.innerWidth || window.screen?.width || 0;
37 | const screenHeight = window.innerHeight || window.screen?.height || 0;
38 | const hasSmallScreen = screenWidth <= 768 || screenHeight <= 768;
39 | if (hasSmallScreen) {
40 | mobileScore += 2;
41 | }
42 |
43 | // Method 3: User agent detection (weight: 3)
44 | const mobileUserAgentPattern = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i;
45 | if (mobileUserAgentPattern.test(userAgent)) {
46 | mobileScore += 3;
47 | }
48 |
49 | // Method 4: Mobile-specific APIs (weight: 2)
50 | const hasMobileAPIs = 'orientation' in window ||
51 | 'DeviceMotionEvent' in window ||
52 | 'DeviceOrientationEvent' in window;
53 | if (hasMobileAPIs) {
54 | mobileScore += 2;
55 | }
56 |
57 | // Method 5: Device pixel ratio (weight: 1)
58 | const hasHighDPI = windowDefined && window.devicePixelRatio > 1.5;
59 | if (hasHighDPI && hasSmallScreen) {
60 | mobileScore += 1;
61 | }
62 |
63 | // Method 6: Viewport meta tag presence (weight: 1)
64 | const hasViewportMeta = document.querySelector('meta[name="viewport"]') !== null;
65 | if (hasViewportMeta && hasSmallScreen) {
66 | mobileScore += 1;
67 | }
68 |
69 | // Method 7: Check for desktop-specific indicators (negative weight)
70 | const hasLargeScreen = screenWidth > 1024 && screenHeight > 768;
71 | const hasKeyboard = 'keyboard' in navigator;
72 | const hasPointer = window.matchMedia && window.matchMedia('(pointer: fine)').matches;
73 |
74 | if (hasLargeScreen && !isTouchDevice) {
75 | mobileScore -= 2;
76 | }
77 | if (hasPointer && !isTouchDevice) {
78 | mobileScore -= 1;
79 | }
80 |
81 | // Method 8: Special case for iPad Pro and similar devices
82 | const isPadWithKeyboard = userAgent.includes('macintosh') && isTouchDevice;
83 | if (isPadWithKeyboard) {
84 | mobileScore += 2;
85 | }
86 |
87 | return mobileScore >= CONFIDENCE_THRESHOLD ? DeviceType.MOBILE : DeviceType.DESKTOP;
88 | }
89 |
90 | /**
91 | * Highly accurate mobile device type detection - returns only 'android' or 'ios'
92 | * Should only be called when getDeviceType() returns 'mobile'
93 | * @returns {DeviceType.ANDROID | DeviceType.IOS} The detected mobile device type
94 | */
95 | export function getMobileDeviceType(): DeviceType.ANDROID | DeviceType.IOS {
96 | // Early return for server-side rendering - default to Android
97 | if (!navigatorDefined || !windowDefined) {
98 | return DeviceType.ANDROID;
99 | }
100 |
101 | const ua = navigator.userAgent;
102 |
103 | // Strategy 1: Direct iOS detection using comprehensive regex
104 | const iosPattern = /iPad|iPhone|iPod/i;
105 | if (iosPattern.test(ua)) {
106 | return DeviceType.IOS;
107 | }
108 |
109 | // Strategy 2: Direct Android detection
110 | const androidPattern = /Android/i;
111 | if (androidPattern.test(ua)) {
112 | return DeviceType.ANDROID;
113 | }
114 |
115 | // Strategy 3: iPad Pro masquerading as Mac detection
116 | const isMacWithTouch = /Macintosh|MacIntel/i.test(ua) && 'ontouchstart' in window;
117 | if (isMacWithTouch) {
118 | return DeviceType.IOS;
119 | }
120 |
121 | // Strategy 4: Modern iPad detection using userAgentData
122 | if (userAgentData?.platform === 'macOS' && 'ontouchstart' in window) {
123 | return DeviceType.IOS;
124 | }
125 |
126 | // Strategy 5: iOS-specific API detection
127 | if (typeof (window as any).DeviceMotionEvent?.requestPermission === 'function') {
128 | return DeviceType.IOS;
129 | }
130 |
131 | // Strategy 6: CSS property detection for iOS
132 | if (typeof CSS !== 'undefined' && CSS.supports?.('-webkit-touch-callout', 'none')) {
133 | return DeviceType.IOS;
134 | }
135 |
136 | // Strategy 7: WebKit without Chrome/Android indicates iOS Safari
137 | const isIOSWebKit = /WebKit/i.test(ua) && !/Chrome|CriOS|Android/i.test(ua);
138 | if (isIOSWebKit) {
139 | return DeviceType.IOS;
140 | }
141 |
142 | // Strategy 8: Chrome detection for Android (when Android not explicitly found)
143 | const isChromeOnMobile = (window as any).chrome && /Mobile/i.test(ua);
144 | if (isChromeOnMobile && !iosPattern.test(ua)) {
145 | return DeviceType.ANDROID;
146 | }
147 |
148 | // Strategy 9: Check for mobile-specific patterns that indicate Android
149 | const androidMobilePattern = /Mobile.*Android|Android.*Mobile/i;
150 | if (androidMobilePattern.test(ua)) {
151 | return DeviceType.ANDROID;
152 | }
153 |
154 | // Strategy 10: Fallback using existing regex patterns
155 | const mobilePattern = /webos|blackberry|iemobile|opera mini/i;
156 | if (mobilePattern.test(ua)) {
157 | // These are typically Android-based or Android-like
158 | return DeviceType.ANDROID;
159 | }
160 |
161 | // Default fallback - Android is more common globally
162 | return DeviceType.ANDROID;
163 | }
164 |
165 | /**
166 | * Convenience method to check if current device is mobile
167 | * @returns {boolean} True if device is mobile
168 | */
169 | export function isMobileDevice(): boolean {
170 | return getDeviceType() === DeviceType.MOBILE;
171 | }
172 |
173 | /**
174 | * Convenience method to check if current device is desktop
175 | * @returns {boolean} True if device is desktop
176 | */
177 | export function isDesktopDevice(): boolean {
178 | return getDeviceType() === DeviceType.DESKTOP;
179 | }
--------------------------------------------------------------------------------
/src/utils/errors.ts:
--------------------------------------------------------------------------------
1 | function createErrorClass(name: string) {
2 | return class extends Error {
3 | constructor(message?: string, public innerError?: Error) {
4 | // Include inner error message in the main message if available
5 | const fullMessage = innerError
6 | ? `${message || ''} caused by ${innerError.name}: ${innerError.message}`
7 | : message;
8 |
9 | super(fullMessage);
10 | this.name = name;
11 | if (innerError) {
12 | this.stack += `\nCaused by: ${innerError.stack}`;
13 | }
14 | }
15 | };
16 | }
17 |
18 | export const TimeoutError = createErrorClass('TimeoutError');
19 | export const ProofNotVerifiedError = createErrorClass('ProofNotVerifiedError');
20 | export const SessionNotStartedError = createErrorClass('SessionNotStartedError');
21 | export const ProviderNotFoundError = createErrorClass('ProviderNotFoundError');
22 | export const SignatureGeneratingError = createErrorClass('SignatureGeneratingError');
23 | export const SignatureNotFoundError = createErrorClass('SignatureNotFoundError');
24 | export const InvalidSignatureError = createErrorClass('InvalidSignatureError');
25 | export const UpdateSessionError = createErrorClass('UpdateSessionError');
26 | export const InitSessionError = createErrorClass('InitSessionError');
27 | export const ProviderFailedError = createErrorClass('ProviderFailedError');
28 | export const InvalidParamError = createErrorClass('InvalidParamError');
29 | export const ApplicationError = createErrorClass('ApplicationError');
30 | export const InitError = createErrorClass('InitError');
31 | export const BackendServerError = createErrorClass('BackendServerError');
32 | export const GetStatusUrlError = createErrorClass('GetStatusUrlError');
33 | export const NoProviderParamsError = createErrorClass('NoProviderParamsError');
34 | export const SetParamsError = createErrorClass('SetParamsError');
35 | export const AddContextError = createErrorClass('AddContextError');
36 | export const SetSignatureError = createErrorClass('SetSignatureError');
37 | export const GetAppCallbackUrlError = createErrorClass("GetAppCallbackUrlError");
38 | export const StatusUrlError = createErrorClass('StatusUrlError');
39 | export const InavlidParametersError = createErrorClass('InavlidParametersError');
40 | export const ProofSubmissionFailedError = createErrorClass('ProofSubmissionFailedError');
--------------------------------------------------------------------------------
/src/utils/helper.ts:
--------------------------------------------------------------------------------
1 | import { OnError } from './types'
2 | import { TimeoutError } from './errors'
3 | import loggerModule from './logger'
4 | const logger = loggerModule.logger
5 |
6 | /**
7 | * Escapes special characters in a string for use in a regular expression
8 | * @param string - The input string to escape
9 | * @returns The input string with special regex characters escaped
10 | */
11 | export function escapeRegExp(string: string): string {
12 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
13 | }
14 |
15 | /**
16 | * Replaces all occurrences of a substring in a string
17 | * @param str - The original string
18 | * @param find - The substring to find
19 | * @param replace - The string to replace the found substrings with
20 | * @returns A new string with all occurrences of 'find' replaced by 'replace'
21 | */
22 | export function replaceAll(str: string, find: string, replace: string): string {
23 | if (find === '') return str;
24 | return str.replace(new RegExp(escapeRegExp(find), 'g'), replace);
25 | }
26 |
27 | /**
28 | * Schedules a task to end an interval after a specified timeout
29 | * @param sessionId - The ID of the current session
30 | * @param intervals - A Map containing the intervals
31 | * @param onFailureCallback - Callback function to be called on failure
32 | * @param timeout - Timeout in milliseconds (default: 10 minutes)
33 | */
34 | export function scheduleIntervalEndingTask(
35 | sessionId: string,
36 | intervals: Map,
37 | onFailureCallback: OnError,
38 | timeout: number = 1000 * 60 * 10
39 | ): void {
40 | setTimeout(() => {
41 | if (intervals.has(sessionId)) {
42 | const message = 'Interval ended without receiving proofs'
43 | onFailureCallback(new TimeoutError(message))
44 | logger.info(message)
45 | clearInterval(intervals.get(sessionId) as NodeJS.Timeout)
46 | intervals.delete(sessionId)
47 | }
48 | }, timeout)
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/interfaces.ts:
--------------------------------------------------------------------------------
1 | // Proof-related interfaces
2 | export interface Proof {
3 | identifier: string;
4 | claimData: ProviderClaimData;
5 | signatures: string[];
6 | witnesses: WitnessData[];
7 | extractedParameterValues: any;
8 | publicData?: { [key: string]: string };
9 | taskId?: number;
10 | }
11 |
12 | // Extension Interactions
13 | export const RECLAIM_EXTENSION_ACTIONS = {
14 | CHECK_EXTENSION: 'RECLAIM_EXTENSION_CHECK',
15 | EXTENSION_RESPONSE: 'RECLAIM_EXTENSION_RESPONSE',
16 | START_VERIFICATION: 'RECLAIM_START_VERIFICATION',
17 | STATUS_UPDATE: 'RECLAIM_STATUS_UPDATE',
18 | };
19 |
20 | export interface ExtensionMessage {
21 | action: string;
22 | messageId: string;
23 | data?: any;
24 | extensionID?: string;
25 | }
26 |
27 | export interface WitnessData {
28 | id: string;
29 | url: string;
30 | }
31 |
32 | export interface ProviderClaimData {
33 | provider: string;
34 | parameters: string;
35 | owner: string;
36 | timestampS: number;
37 | context: string;
38 | identifier: string;
39 | epoch: number;
40 | }
41 |
42 | // Context and Beacon interfaces
43 | export interface Context {
44 | contextAddress: string;
45 | contextMessage: string;
46 | }
47 |
48 | export interface Beacon {
49 | getState(epoch?: number): Promise;
50 | close?(): Promise;
51 | }
52 |
53 | export type BeaconState = {
54 | witnesses: WitnessData[];
55 | epoch: number;
56 | witnessesRequiredForClaim: number;
57 | nextEpochTimestampS: number;
58 | };
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | // Define the possible log levels
2 | export type LogLevel = 'info' | 'warn' | 'error' | 'silent';
3 |
4 | // Define a simple logger class
5 | class SimpleLogger {
6 | private level: LogLevel = 'info';
7 |
8 | setLevel(level: LogLevel) {
9 | this.level = level;
10 | }
11 |
12 | private shouldLog(messageLevel: LogLevel): boolean {
13 | const levels: LogLevel[] = ['error', 'warn', 'info', 'silent'];
14 | return levels.indexOf(this.level) >= levels.indexOf(messageLevel);
15 | }
16 |
17 | private log(level: LogLevel, message: string, ...args: any[]) {
18 | if (this.shouldLog(level) && this.level !== 'silent') {
19 | const logFunction = this.getLogFunction(level);
20 | console.log('current level', this.level);
21 | logFunction(`[${level.toUpperCase()}]`, message, ...args);
22 | }
23 | }
24 |
25 | private getLogFunction(level: LogLevel): (message?: any, ...optionalParams: any[]) => void {
26 | switch (level) {
27 | case 'error':
28 | return console.error;
29 | case 'warn':
30 | return console.warn;
31 | case 'info':
32 | return console.info;
33 | default:
34 | return () => {}; // No-op for 'silent'
35 | }
36 | }
37 |
38 | info(message: string, ...args: any[]) {
39 | this.log('info', message, ...args);
40 | }
41 |
42 | warn(message: string, ...args: any[]) {
43 | this.log('warn', message, ...args);
44 | }
45 |
46 | error(message: string, ...args: any[]) {
47 | this.log('error', message, ...args);
48 | }
49 | }
50 |
51 | // Create the logger instance
52 | const logger = new SimpleLogger();
53 |
54 | // Function to set the log level
55 | export function setLogLevel(level: LogLevel) {
56 | logger.setLevel(level);
57 | }
58 |
59 | // Export the logger instance and the setLogLevel function
60 | export default {
61 | logger,
62 | setLogLevel
63 | };
64 |
--------------------------------------------------------------------------------
/src/utils/modalUtils.ts:
--------------------------------------------------------------------------------
1 | import loggerModule from './logger';
2 | import { ModalOptions } from './types';
3 | import { constants } from './constants';
4 | const logger = loggerModule.logger;
5 |
6 | export class QRCodeModal {
7 | private modalId: string;
8 | private options: ModalOptions;
9 | private autoCloseTimer?: NodeJS.Timeout;
10 | private countdownTimer?: NodeJS.Timeout;
11 | private countdownSeconds: number = 60;
12 |
13 | constructor(options: ModalOptions = {}) {
14 | this.modalId = 'reclaim-qr-modal';
15 | this.options = {
16 | title: 'Verify with Reclaim',
17 | description: 'Scan the QR code with your mobile device to complete verification',
18 | extensionUrl: constants.CHROME_EXTENSION_URL,
19 | darkTheme: false,
20 | ...options
21 | };
22 | }
23 |
24 | async show(requestUrl: string): Promise {
25 | try {
26 | // Remove existing modal if present
27 | this.close();
28 |
29 | // Create modal HTML
30 | const modalHTML = this.createModalHTML();
31 |
32 | // Add modal to DOM
33 | document.body.insertAdjacentHTML('beforeend', modalHTML);
34 |
35 | // Generate QR code
36 | await this.generateQRCode(requestUrl, 'reclaim-qr-code');
37 |
38 | // Add event listeners
39 | this.addEventListeners();
40 |
41 | // Start auto-close timer
42 | this.startAutoCloseTimer();
43 |
44 | } catch (error) {
45 | logger.info('Error showing QR code modal:', error);
46 | throw error;
47 | }
48 | }
49 |
50 | close(): void {
51 | // Clear timers
52 | if (this.autoCloseTimer) {
53 | clearTimeout(this.autoCloseTimer);
54 | this.autoCloseTimer = undefined;
55 | }
56 | if (this.countdownTimer) {
57 | clearInterval(this.countdownTimer);
58 | this.countdownTimer = undefined;
59 | }
60 |
61 | const modal = document.getElementById(this.modalId);
62 | if (modal) {
63 | modal.remove();
64 | }
65 | if (this.options.onClose) {
66 | this.options.onClose();
67 | }
68 | }
69 |
70 | private getThemeStyles() {
71 | const isDark = this.options.darkTheme;
72 |
73 | return {
74 | modalBackground: isDark ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)',
75 | cardBackground: isDark ? '#1f2937' : 'white',
76 | titleColor: isDark ? '#f9fafb' : '#1f2937',
77 | textColor: isDark ? '#d1d5db' : '#6b7280',
78 | qrBackground: isDark ? '#374151' : '#f9fafb',
79 | tipBackground: isDark ? '#1e40af' : '#f0f9ff',
80 | tipBorder: isDark ? '#1e40af' : '#e0f2fe',
81 | tipTextColor: isDark ? '#dbeafe' : '#0369a1',
82 | buttonBackground: isDark ? '#374151' : '#f3f4f6',
83 | buttonColor: isDark ? '#f9fafb' : '#374151',
84 | buttonHoverBackground: isDark ? '#4b5563' : '#e5e7eb',
85 | countdownColor: isDark ? '#6b7280' : '#9ca3af',
86 | progressBackground: isDark ? '#4b5563' : '#e5e7eb',
87 | progressGradient: isDark
88 | ? 'linear-gradient(90deg, #3b82f6 0%, #2563eb 50%, #1d4ed8 100%)'
89 | : 'linear-gradient(90deg, #2563eb 0%, #1d4ed8 50%, #1e40af 100%)',
90 | linkColor: isDark ? '#60a5fa' : '#2563eb',
91 | extensionButtonBackground: isDark ? '#1e40af' : '#2563eb',
92 | extensionButtonHover: isDark ? '#1d4ed8' : '#1d4ed8'
93 | };
94 | }
95 |
96 | private createModalHTML(): string {
97 | const styles = this.getThemeStyles();
98 |
99 | return `
100 |
113 |
123 |
146 |
147 |
${this.options.title}
153 |
154 |
${this.options.description}
160 |
161 |
167 |
168 |
175 |
💡 For a better experience
181 |
Install our browser extension for seamless verification without QR codes
187 |
202 | Install Extension
203 |
204 |
205 |
206 |
207 |
Auto-close in 1:00
213 |
214 |
229 |
230 |
231 |
232 | `
233 | }
234 |
235 | private async generateQRCode(text: string, containerId: string): Promise {
236 | try {
237 | // Simple QR code generation using a public API
238 | // In production, you might want to use a proper QR code library
239 | const qrCodeUrl = `${constants.QR_CODE_API_URL}?size=200x200&data=${encodeURIComponent(text)}`;
240 |
241 | const container = document.getElementById(containerId);
242 | const styles = this.getThemeStyles();
243 |
244 | if (container) {
245 | container.innerHTML = `
246 |
250 |
256 | `;
257 | }
258 | } catch (error) {
259 | logger.info('Error generating QR code:', error);
260 | // Fallback to text link
261 | const container = document.getElementById(containerId);
262 | const styles = this.getThemeStyles();
263 |
264 | if (container) {
265 | container.innerHTML = `
266 |
271 | `;
272 | }
273 | }
274 | }
275 |
276 | private addEventListeners(): void {
277 | const closeButton = document.getElementById('reclaim-close-modal');
278 | const modal = document.getElementById(this.modalId);
279 |
280 | const closeModal = () => {
281 | this.close();
282 | };
283 |
284 | if (closeButton) {
285 | closeButton.addEventListener('click', closeModal);
286 | }
287 |
288 | // Close on backdrop click
289 | if (modal) {
290 | modal.addEventListener('click', (e) => {
291 | if (e.target === modal) {
292 | closeModal();
293 | }
294 | });
295 | }
296 |
297 | // Close on escape key
298 | const handleEscape = (e: KeyboardEvent) => {
299 | if (e.key === 'Escape') {
300 | closeModal();
301 | document.removeEventListener('keydown', handleEscape);
302 | }
303 | };
304 | document.addEventListener('keydown', handleEscape);
305 | }
306 |
307 | private startAutoCloseTimer(): void {
308 | this.countdownSeconds = 60;
309 |
310 | // Update countdown display immediately
311 | this.updateCountdownDisplay();
312 |
313 | // Start countdown timer (updates every second)
314 | this.countdownTimer = setInterval(() => {
315 | this.countdownSeconds--;
316 | this.updateCountdownDisplay();
317 |
318 | if (this.countdownSeconds <= 0) {
319 | this.close();
320 | }
321 | }, 1000);
322 |
323 | // Set auto-close timer for 1 minute
324 | this.autoCloseTimer = setTimeout(() => {
325 | this.close();
326 | }, 60000);
327 | }
328 |
329 | private updateCountdownDisplay(): void {
330 | const countdownElement = document.getElementById('reclaim-countdown');
331 | const progressBar = document.getElementById('reclaim-progress-bar');
332 |
333 | if (countdownElement) {
334 | const minutes = Math.floor(this.countdownSeconds / 60);
335 | const seconds = this.countdownSeconds % 60;
336 | const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`;
337 | countdownElement.textContent = `Auto-close in ${timeString}`;
338 | }
339 |
340 | if (progressBar) {
341 | // Calculate progress percentage (reverse: starts at 100%, goes to 0%)
342 | const progressPercentage = (this.countdownSeconds / 60) * 100;
343 | progressBar.style.width = `${progressPercentage}%`;
344 | }
345 | }
346 | }
--------------------------------------------------------------------------------
/src/utils/proofUtils.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { WitnessData } from "./interfaces";
3 | import { SignedClaim, TemplateData } from "./types";
4 | import { createSignDataForClaim, fetchWitnessListForClaim } from "../witness";
5 | import { BACKEND_BASE_URL, constants } from "./constants";
6 | import { replaceAll } from "./helper";
7 | import { validateURL } from "./validationUtils";
8 | import { makeBeacon } from "../smart-contract";
9 | import { ProofNotVerifiedError } from "./errors";
10 | import loggerModule from './logger';
11 | const logger = loggerModule.logger;
12 |
13 |
14 | /**
15 | * Retrieves a shortened URL for the given URL
16 | * @param url - The URL to be shortened
17 | * @returns A promise that resolves to the shortened URL, or the original URL if shortening fails
18 | */
19 | export async function getShortenedUrl(url: string): Promise {
20 | logger.info(`Attempting to shorten URL: ${url}`);
21 | try {
22 | validateURL(url, 'getShortenedUrl')
23 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/shortener`, {
24 | method: 'POST',
25 | headers: { 'Content-Type': 'application/json' },
26 | body: JSON.stringify({ fullUrl: url })
27 | })
28 | const res = await response.json()
29 | if (!response.ok) {
30 | logger.info(`Failed to shorten URL: ${url}, Response: ${JSON.stringify(res)}`);
31 | return url;
32 | }
33 | const shortenedVerificationUrl = res.result.shortUrl
34 | return shortenedVerificationUrl
35 | } catch (err) {
36 | logger.info(`Error shortening URL: ${url}, Error: ${err}`);
37 | return url
38 | }
39 | }
40 |
41 | /**
42 | * Creates a link with embedded template data
43 | * @param templateData - The data to be embedded in the link
44 | * @returns A promise that resolves to the created link (shortened if possible)
45 | */
46 | export async function createLinkWithTemplateData(templateData: TemplateData): Promise {
47 | let template = encodeURIComponent(JSON.stringify(templateData))
48 | template = replaceAll(template, '(', '%28')
49 | template = replaceAll(template, ')', '%29')
50 |
51 | const fullLink = `${constants.RECLAIM_SHARE_URL}${template}`
52 | try {
53 | const shortenedLink = await getShortenedUrl(fullLink)
54 | return shortenedLink;
55 | } catch (err) {
56 | logger.info(`Error creating link for sessionId: ${templateData.sessionId}, Error: ${err}`);
57 | return fullLink;
58 | }
59 | }
60 |
61 | /**
62 | * Retrieves the list of witnesses for a given claim
63 | * @param epoch - The epoch number
64 | * @param identifier - The claim identifier
65 | * @param timestampS - The timestamp in seconds
66 | * @returns A promise that resolves to an array of witness addresses
67 | * @throws Error if no beacon is available
68 | */
69 | export async function getWitnessesForClaim(
70 | epoch: number,
71 | identifier: string,
72 | timestampS: number
73 | ): Promise {
74 | const beacon = makeBeacon()
75 | if (!beacon) {
76 | logger.info('No beacon available for getting witnesses');
77 | throw new Error('No beacon available');
78 | }
79 | const state = await beacon.getState(epoch)
80 | const witnessList = fetchWitnessListForClaim(state, identifier, timestampS)
81 | const witnesses = witnessList.map((w: WitnessData) => w.id.toLowerCase())
82 | return witnesses;
83 | }
84 |
85 | /**
86 | * Recovers the signers' addresses from a signed claim
87 | * @param claim - The signed claim object
88 | * @param signatures - The signatures associated with the claim
89 | * @returns An array of recovered signer addresses
90 | */
91 | export function recoverSignersOfSignedClaim({
92 | claim,
93 | signatures
94 | }: SignedClaim): string[] {
95 | const dataStr = createSignDataForClaim({ ...claim })
96 | const signers = signatures.map(signature =>
97 | ethers.verifyMessage(dataStr, ethers.hexlify(signature)).toLowerCase()
98 | )
99 | return signers;
100 | }
101 |
102 | /**
103 | * Asserts that a signed claim is valid by checking if all expected witnesses have signed
104 | * @param claim - The signed claim to validate
105 | * @param expectedWitnessAddresses - An array of expected witness addresses
106 | * @throws ProofNotVerifiedError if any expected witness signature is missing
107 | */
108 | export function assertValidSignedClaim(
109 | claim: SignedClaim,
110 | expectedWitnessAddresses: string[]
111 | ): void {
112 | const witnessAddresses = recoverSignersOfSignedClaim(claim)
113 | const witnessesNotSeen = new Set(expectedWitnessAddresses)
114 | for (const witness of witnessAddresses) {
115 | if (witnessesNotSeen.has(witness)) {
116 | witnessesNotSeen.delete(witness)
117 | }
118 | }
119 |
120 | if (witnessesNotSeen.size > 0) {
121 | const missingWitnesses = Array.from(witnessesNotSeen).join(', ');
122 | logger.info(`Claim validation failed. Missing signatures from: ${missingWitnesses}`);
123 | throw new ProofNotVerifiedError(
124 | `Missing signatures from ${missingWitnesses}`
125 | )
126 | }
127 | }
--------------------------------------------------------------------------------
/src/utils/sessionUtils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | InitSessionError,
3 | UpdateSessionError,
4 | StatusUrlError
5 | } from "./errors";
6 | import { InitSessionResponse, SessionStatus, StatusUrlResponse } from "./types";
7 | import { validateFunctionParams } from "./validationUtils";
8 | import { BACKEND_BASE_URL, constants } from './constants';
9 | import loggerModule from './logger';
10 | const logger = loggerModule.logger;
11 |
12 | /**
13 | * Initializes a session with the provided parameters
14 | * @param providerId - The ID of the provider
15 | * @param appId - The ID of the application
16 | * @param timestamp - The timestamp of the request
17 | * @param signature - The signature for authentication
18 | * @returns A promise that resolves to an InitSessionResponse
19 | * @throws InitSessionError if the session initialization fails
20 | */
21 | export async function initSession(
22 | providerId: string,
23 | appId: string,
24 | timestamp: string,
25 | signature: string,
26 | versionNumber?: string
27 | ): Promise {
28 | logger.info(`Initializing session for providerId: ${providerId}, appId: ${appId}`);
29 | try {
30 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/init/session/`, {
31 | method: 'POST',
32 | headers: { 'Content-Type': 'application/json' },
33 | body: JSON.stringify({ providerId, appId, timestamp, signature, versionNumber })
34 | });
35 |
36 | const res = await response.json();
37 |
38 | if (!response.ok) {
39 | logger.info(`Session initialization failed: ${res.message || 'Unknown error'}`);
40 | throw new InitSessionError(res.message || `Error initializing session with providerId: ${providerId}`);
41 | }
42 |
43 | return res as InitSessionResponse;
44 | } catch (err) {
45 | logger.info(`Failed to initialize session for providerId: ${providerId}, appId: ${appId}`, err);
46 | throw err;
47 | }
48 | }
49 |
50 | /**
51 | * Updates the status of an existing session
52 | * @param sessionId - The ID of the session to update
53 | * @param status - The new status of the session
54 | * @returns A promise that resolves to the update response
55 | * @throws UpdateSessionError if the session update fails
56 | */
57 | export async function updateSession(sessionId: string, status: SessionStatus) {
58 | logger.info(`Updating session status for sessionId: ${sessionId}, new status: ${status}`);
59 | validateFunctionParams(
60 | [{ input: sessionId, paramName: 'sessionId', isString: true }],
61 | 'updateSession'
62 | );
63 |
64 | try {
65 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/update/session/`, {
66 | method: 'POST',
67 | headers: { 'Content-Type': 'application/json' },
68 | body: JSON.stringify({ sessionId, status })
69 | });
70 |
71 | const res = await response.json();
72 |
73 | if (!response.ok) {
74 | const errorMessage = `Error updating session with sessionId: ${sessionId}. Status Code: ${response.status}`;
75 | logger.info(errorMessage, res);
76 | throw new UpdateSessionError(errorMessage);
77 | }
78 |
79 | logger.info(`Session status updated successfully for sessionId: ${sessionId}`);
80 | return res;
81 | } catch (err) {
82 | const errorMessage = `Failed to update session with sessionId: ${sessionId}`;
83 | logger.info(errorMessage, err);
84 | throw new UpdateSessionError(`Error updating session with sessionId: ${sessionId}`);
85 | }
86 | }
87 |
88 | /**
89 | * Fetches the status URL for a given session ID
90 | * @param sessionId - The ID of the session to fetch the status URL for
91 | * @returns A promise that resolves to a StatusUrlResponse
92 | * @throws StatusUrlError if the status URL fetch fails
93 | */
94 | export async function fetchStatusUrl(sessionId: string): Promise {
95 | validateFunctionParams(
96 | [{ input: sessionId, paramName: 'sessionId', isString: true }],
97 | 'fetchStatusUrl'
98 | );
99 |
100 | try {
101 | const response = await fetch(`${constants.DEFAULT_RECLAIM_STATUS_URL}${sessionId}`, {
102 | method: 'GET',
103 | headers: { 'Content-Type': 'application/json' }
104 | });
105 |
106 | const res = await response.json();
107 |
108 | if (!response.ok) {
109 | const errorMessage = `Error fetching status URL for sessionId: ${sessionId}. Status Code: ${response.status}`;
110 | logger.info(errorMessage, res);
111 | throw new StatusUrlError(errorMessage);
112 | }
113 |
114 | return res as StatusUrlResponse;
115 | } catch (err) {
116 | const errorMessage = `Failed to fetch status URL for sessionId: ${sessionId}`;
117 | logger.info(errorMessage, err);
118 | throw new StatusUrlError(`Error fetching status URL for sessionId: ${sessionId}`);
119 | }
120 | }
121 |
122 |
123 |
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Proof, ProviderClaimData } from './interfaces';
2 | import type { ParsedQs } from 'qs';
3 |
4 | // Claim-related types
5 | export type ClaimID = ProviderClaimData['identifier'];
6 |
7 | export type ClaimInfo = Pick;
8 |
9 | export type AnyClaimInfo = ClaimInfo | { identifier: ClaimID };
10 |
11 | export type CompleteClaimData = Pick & AnyClaimInfo;
12 |
13 | export type SignedClaim = {
14 | claim: CompleteClaimData;
15 | signatures: Uint8Array[];
16 | };
17 |
18 | // Request and session-related types
19 | export type CreateVerificationRequest = {
20 | providerIds: string[];
21 | applicationSecret?: string;
22 | };
23 |
24 | export type StartSessionParams = {
25 | onSuccess: OnSuccess;
26 | onError: OnError;
27 | };
28 |
29 | export type OnSuccess = (proof?: Proof | Proof[] | string) => void;
30 | export type OnError = (error: Error) => void;
31 |
32 | export type ProofRequestOptions = {
33 | log?: boolean;
34 | acceptAiProviders?: boolean;
35 | useAppClip?: boolean;
36 | device?: string;
37 | envUrl?: string;
38 | useBrowserExtension?: boolean;
39 | extensionID?: string;
40 | providerVersion?: string;
41 | };
42 |
43 | // Modal customization options
44 | export type ModalOptions = {
45 | title?: string;
46 | description?: string;
47 | extensionUrl?: string;
48 | darkTheme?: boolean;
49 | onClose?: () => void;
50 | };
51 |
52 | // Claim creation type enum
53 | export enum ClaimCreationType {
54 | STANDALONE = 'createClaim',
55 | ON_ME_CHAIN = 'createClaimOnMechain'
56 | }
57 |
58 | // Device type enum
59 | export enum DeviceType {
60 | ANDROID = 'android',
61 | IOS = 'ios',
62 | DESKTOP = 'desktop',
63 | MOBILE = 'mobile'
64 | }
65 |
66 |
67 | // Session and response types
68 | export type InitSessionResponse = {
69 | sessionId: string;
70 | resolvedProviderVersion: string;
71 | };
72 |
73 | export interface UpdateSessionResponse {
74 | success: boolean;
75 | message?: string;
76 | };
77 |
78 | export enum SessionStatus {
79 | SESSION_INIT = 'SESSION_INIT',
80 | SESSION_STARTED = 'SESSION_STARTED',
81 | USER_INIT_VERIFICATION = 'USER_INIT_VERIFICATION',
82 | USER_STARTED_VERIFICATION = 'USER_STARTED_VERIFICATION',
83 | PROOF_GENERATION_STARTED = 'PROOF_GENERATION_STARTED',
84 | PROOF_GENERATION_SUCCESS = 'PROOF_GENERATION_SUCCESS',
85 | PROOF_GENERATION_FAILED = 'PROOF_GENERATION_FAILED',
86 | PROOF_SUBMITTED = 'PROOF_SUBMITTED',
87 | PROOF_SUBMISSION_FAILED = 'PROOF_SUBMISSION_FAILED',
88 | PROOF_MANUAL_VERIFICATION_SUBMITED = 'PROOF_MANUAL_VERIFICATION_SUBMITED',
89 | };
90 |
91 | // JSON and template-related types
92 | export type ProofPropertiesJSON = {
93 | applicationId: string;
94 | providerId: string;
95 | sessionId: string;
96 | context: Context;
97 | signature: string;
98 | redirectUrl?: string;
99 | parameters: { [key: string]: string };
100 | timeStamp: string;
101 | appCallbackUrl?: string;
102 | claimCreationType?: ClaimCreationType;
103 | options?: ProofRequestOptions;
104 | sdkVersion: string;
105 | jsonProofResponse?: boolean;
106 | resolvedProviderVersion: string;
107 | };
108 |
109 | export type TemplateData = {
110 | sessionId: string;
111 | providerId: string;
112 | applicationId: string;
113 | signature: string;
114 | timestamp: string;
115 | callbackUrl: string;
116 | context: string;
117 | parameters: { [key: string]: string };
118 | redirectUrl: string;
119 | acceptAiProviders: boolean;
120 | sdkVersion: string;
121 | jsonProofResponse?: boolean;
122 | providerVersion?: string;
123 | resolvedProviderVersion: string;
124 | };
125 |
126 | // Add the new StatusUrlResponse type
127 | export type StatusUrlResponse = {
128 | message: string;
129 | session?: {
130 | id: string;
131 | appId: string;
132 | httpProviderId: string[];
133 | sessionId: string;
134 | proofs?: Proof[];
135 | statusV2: string;
136 | };
137 | providerId?: string;
138 | };
--------------------------------------------------------------------------------
/src/utils/validationUtils.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { InavlidParametersError, InvalidParamError, InvalidSignatureError } from "./errors";
3 | import canonicalize from 'canonicalize'
4 | import { Context } from "./interfaces";
5 | import loggerModule from './logger';
6 | import { ProofRequestOptions } from "./types";
7 | const logger = loggerModule.logger;
8 |
9 | /**
10 | * Validates function parameters based on specified criteria
11 | * @param params - An array of objects containing input, paramName, and optional isString flag
12 | * @param functionName - The name of the function being validated
13 | * @throws InvalidParamError if any parameter fails validation
14 | */
15 | export function validateFunctionParams(params: { input: any, paramName: string, isString?: boolean }[], functionName: string): void {
16 | params.forEach(({ input, paramName, isString }) => {
17 | if (input == null) {
18 | logger.info(`Validation failed: ${paramName} in ${functionName} is null or undefined`);
19 | throw new InvalidParamError(`${paramName} passed to ${functionName} must not be null or undefined.`);
20 | }
21 | if (isString && typeof input !== 'string') {
22 | logger.info(`Validation failed: ${paramName} in ${functionName} is not a string`);
23 | throw new InvalidParamError(`${paramName} passed to ${functionName} must be a string.`);
24 | }
25 | if (isString && input.trim() === '') {
26 | logger.info(`Validation failed: ${paramName} in ${functionName} is an empty string`);
27 | throw new InvalidParamError(`${paramName} passed to ${functionName} must not be an empty string.`);
28 | }
29 | });
30 | }
31 |
32 |
33 | // validate the parameters
34 | /**
35 | * Validates the parameters object
36 | * @param parameters - The parameters object to validate
37 | * @throws InavlidParametersError if the parameters object is not valid
38 | */
39 | export function validateParameters(parameters: { [key: string]: string }): void {
40 | try {
41 | // check if the parameters is an object of key value pairs of string and string
42 | if (typeof parameters !== 'object' || parameters === null) {
43 | logger.info(`Parameters validation failed: Provided parameters is not an object`);
44 | throw new InavlidParametersError(`The provided parameters is not an object`);
45 | }
46 | // check each key and value in the parameters object
47 | for (const [key, value] of Object.entries(parameters)) {
48 | if (typeof key !== 'string' || typeof value !== 'string') {
49 | logger.info(`Parameters validation failed: Provided parameters is not an object of key value pairs of string and string`);
50 | throw new InavlidParametersError(`The provided parameters is not an object of key value pairs of string and string`);
51 | }
52 | }
53 | } catch (e) {
54 | logger.info(`Parameters validation failed: ${(e as Error).message}`);
55 | throw new InavlidParametersError(`Invalid parameters passed to validateParameters.`, e as Error);
56 | }
57 | }
58 |
59 |
60 | /**
61 | * Validates a URL string
62 | * @param url - The URL to validate
63 | * @param functionName - The name of the function calling this validation
64 | * @throws InvalidParamError if the URL is invalid or empty
65 | */
66 | export function validateURL(url: string, functionName: string): void {
67 | try {
68 | new URL(url);
69 | } catch (e) {
70 | logger.info(`URL validation failed for ${url} in ${functionName}: ${(e as Error).message}`);
71 | throw new InvalidParamError(`Invalid URL format ${url} passed to ${functionName}.`, e as Error);
72 | }
73 | }
74 |
75 | /**
76 | * Validates a signature against the provided application ID
77 | * @param providerId - The ID of the provider
78 | * @param signature - The signature to validate
79 | * @param applicationId - The expected application ID
80 | * @param timestamp - The timestamp of the signature
81 | * @throws InvalidSignatureError if the signature is invalid or doesn't match the application ID
82 | */
83 | export function validateSignature(providerId: string, signature: string, applicationId: string, timestamp: string): void {
84 | try {
85 | logger.info(`Starting signature validation for providerId: ${providerId}, applicationId: ${applicationId}, timestamp: ${timestamp}`);
86 |
87 | const message = canonicalize({ providerId, timestamp });
88 | if (!message) {
89 | logger.info('Failed to canonicalize message for signature validation');
90 | throw new Error('Failed to canonicalize message');
91 | }
92 | const messageHash = ethers.keccak256(new TextEncoder().encode(message));
93 | let appId = ethers.verifyMessage(
94 | ethers.getBytes(messageHash),
95 | ethers.hexlify(signature)
96 | ).toLowerCase();
97 |
98 | if (ethers.getAddress(appId) !== ethers.getAddress(applicationId)) {
99 | logger.info(`Signature validation failed: Mismatch between derived appId (${appId}) and provided applicationId (${applicationId})`);
100 | throw new InvalidSignatureError(`Signature does not match the application id: ${appId}`);
101 | }
102 |
103 | logger.info(`Signature validated successfully for applicationId: ${applicationId}`);
104 | } catch (err) {
105 | logger.info(`Signature validation failed: ${(err as Error).message}`);
106 | if (err instanceof InvalidSignatureError) {
107 | throw err;
108 | }
109 | throw new InvalidSignatureError(`Failed to validate signature: ${(err as Error).message}`);
110 | }
111 | }
112 |
113 |
114 | /**
115 | * Validates the context object
116 | * @param context - The context object to validate
117 | * @throws InvalidParamError if the context object is not valid
118 | */
119 | export function validateContext(context: Context): void {
120 | if (!context.contextAddress) {
121 | logger.info(`Context validation failed: Provided context address in context is not valid`);
122 | throw new InvalidParamError(`The provided context address in context is not valid`);
123 | }
124 |
125 | if (!context.contextMessage) {
126 | logger.info(`Context validation failed: Provided context message in context is not valid`);
127 | throw new InvalidParamError(`The provided context message in context is not valid`);
128 | }
129 |
130 | validateFunctionParams([
131 | { input: context.contextAddress, paramName: 'contextAddress', isString: true },
132 | { input: context.contextMessage, paramName: 'contextMessage', isString: true }
133 | ], 'validateContext');
134 | }
135 |
136 | /**
137 | * Validates the options object
138 | * @param options - The options object to validate
139 | * @throws InvalidParamError if the options object is not valid
140 | */
141 | export function validateOptions(options: ProofRequestOptions): void {
142 | if (options.acceptAiProviders && typeof options.acceptAiProviders !== 'boolean') {
143 | logger.info(`Options validation failed: Provided acceptAiProviders in options is not valid`);
144 | throw new InvalidParamError(`The provided acceptAiProviders in options is not valid`);
145 | }
146 |
147 | if (options.log && typeof options.log !== 'boolean') {
148 | logger.info(`Options validation failed: Provided log in options is not valid`);
149 | throw new InvalidParamError(`The provided log in options is not valid`);
150 | }
151 |
152 | if (options.providerVersion && typeof options.providerVersion !== 'string') {
153 | logger.info(`Options validation failed: Provided providerVersion in options is not valid`);
154 | throw new InvalidParamError(`The provided providerVersion in options is not valid`);
155 | }
156 | }
157 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/src/witness.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import type { WitnessData } from './utils/interfaces';
3 | import type { ClaimID, ClaimInfo, CompleteClaimData } from './utils/types';
4 |
5 | type BeaconState = {
6 | witnesses: WitnessData[];
7 | epoch: number;
8 | witnessesRequiredForClaim: number;
9 | nextEpochTimestampS: number;
10 | };
11 |
12 | export function fetchWitnessListForClaim(
13 | { witnesses, witnessesRequiredForClaim, epoch }: BeaconState,
14 | params: string | ClaimInfo,
15 | timestampS: number
16 | ): WitnessData[] {
17 | const identifier: ClaimID =
18 | typeof params === 'string' ? params : getIdentifierFromClaimInfo(params);
19 | const completeInput: string = [
20 | identifier,
21 | epoch.toString(),
22 | witnessesRequiredForClaim.toString(),
23 | timestampS.toString(),
24 | ].join('\n');
25 | const completeHashStr: string = ethers.keccak256(strToUint8Array(completeInput));
26 | const completeHash: Uint8Array = ethers.getBytes(completeHashStr);
27 | const completeHashView: DataView = uint8ArrayToDataView(completeHash);
28 | const witnessesLeft: WitnessData[] = [...witnesses];
29 | const selectedWitnesses: WitnessData[] = [];
30 | let byteOffset: number = 0;
31 | for (let i = 0; i < witnessesRequiredForClaim; i++) {
32 | const randomSeed: number = completeHashView.getUint32(byteOffset);
33 | const witnessIndex: number = randomSeed % witnessesLeft.length;
34 | const witness: WitnessData = witnessesLeft[witnessIndex];
35 | selectedWitnesses.push(witness);
36 |
37 | witnessesLeft[witnessIndex] = witnessesLeft[witnessesLeft.length - 1];
38 | witnessesLeft.pop();
39 | byteOffset = (byteOffset + 4) % completeHash.length;
40 | }
41 |
42 | return selectedWitnesses;
43 | }
44 |
45 | export function getIdentifierFromClaimInfo(info: ClaimInfo): ClaimID {
46 | const str: string = `${info.provider}\n${info.parameters}\n${info.context || ''}`;
47 | return ethers.keccak256(strToUint8Array(str)).toLowerCase();
48 | }
49 |
50 | export function strToUint8Array(str: string): Uint8Array {
51 | return new TextEncoder().encode(str);
52 | }
53 |
54 | export function uint8ArrayToDataView(arr: Uint8Array): DataView {
55 | return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
56 | }
57 |
58 | export function createSignDataForClaim(data: CompleteClaimData): string {
59 | const identifier: ClaimID =
60 | 'identifier' in data ? data.identifier : getIdentifierFromClaimInfo(data);
61 | const lines: string[] = [
62 | identifier,
63 | data.owner.toLowerCase(),
64 | data.timestampS.toString(),
65 | data.epoch.toString(),
66 | ];
67 |
68 | return lines.join('\n');
69 | }
70 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 | "outDir": "./dist",
5 | "declaration": true,
6 | "emitDeclarationOnly": false,
7 | "declarationMap": true,
8 |
9 | /* Language and Environment */
10 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
11 |
12 | /* Modules */
13 | "module": "commonjs" /* Specify what module code is generated. */,
14 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
15 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
16 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
17 | "resolveJsonModule": true,
18 | /* Type Checking */
19 | "strict": true /* Enable all strict type-checking options. */,
20 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
21 | },
22 | "exclude": ["example"]
23 | }
24 |
--------------------------------------------------------------------------------