prividium 0.0.1-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +327 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/popup-auth.d.ts +38 -0
- package/dist/popup-auth.js +203 -0
- package/dist/prividium-chain.d.ts +2 -0
- package/dist/prividium-chain.js +98 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.js +82 -0
- package/dist/token-utils.d.ts +4 -0
- package/dist/token-utils.js +55 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +13 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Prividium SDK
|
|
2
|
+
|
|
3
|
+
A TypeScript SDK for integrating with Prividium's authorization system, providing seamless authentication and secure RPC
|
|
4
|
+
communication for blockchain applications.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- 🔐 **Popup-based OAuth Authentication** - Secure authentication flow using popup windows
|
|
9
|
+
- 🔑 **JWT Token Management** - Automatic token storage, validation, and expiration handling
|
|
10
|
+
- 🌐 **Viem Integration** - Drop-in transport for viem clients with automatic auth headers
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install prividium
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### 1. Create a Prividium Chain
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createPrividiumChain } from 'prividium';
|
|
24
|
+
import { defineChain, createPublicClient, http } from 'viem';
|
|
25
|
+
|
|
26
|
+
// Define your chain
|
|
27
|
+
const prividiumChain = defineChain({
|
|
28
|
+
id: 7777,
|
|
29
|
+
name: 'Prividium Chain',
|
|
30
|
+
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
31
|
+
rpcUrls: { default: { http: [] } },
|
|
32
|
+
blockExplorers: { default: { name: 'Explorer', url: 'https://explorer.prividium.io' } }
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Create SDK instance
|
|
36
|
+
const prividium = createPrividiumChain({
|
|
37
|
+
clientId: 'your-client-id',
|
|
38
|
+
chain: prividiumChain,
|
|
39
|
+
rpcUrl: 'https://rpc.prividium.io',
|
|
40
|
+
authBaseUrl: 'https://auth.prividium.io',
|
|
41
|
+
redirectUrl: window.location.origin + '/auth/callback',
|
|
42
|
+
onAuthExpiry: () => {
|
|
43
|
+
console.log('Authentication expired - please reconnect');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Create Viem Client
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// The SDK provides a pre-configured transport with automatic auth headers
|
|
52
|
+
const client = createPublicClient({
|
|
53
|
+
chain: prividium.chain,
|
|
54
|
+
transport: prividium.transport // ✨ Auth headers are automatically included!
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Authenticate and Use
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// Check if already authenticated
|
|
62
|
+
if (!prividium.isAuthorized()) {
|
|
63
|
+
// Trigger authentication popup
|
|
64
|
+
await prividium.authorize();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Now you can make authenticated RPC calls
|
|
68
|
+
const balance = await client.getBalance({
|
|
69
|
+
address: '0x...'
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 4. Setup OAuth Callback Page
|
|
74
|
+
|
|
75
|
+
The SDK requires a callback page to complete the authentication flow securely using `postMessage`. Create a callback
|
|
76
|
+
page at the `redirectUrl` you configured:
|
|
77
|
+
|
|
78
|
+
**Example: `public/auth/callback.html`**
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<!DOCTYPE html>
|
|
82
|
+
<html>
|
|
83
|
+
<head>
|
|
84
|
+
<title>Authentication Callback</title>
|
|
85
|
+
<script type="module">
|
|
86
|
+
import { handleAuthCallback } from '@repo/prividium-sdk';
|
|
87
|
+
|
|
88
|
+
// Handle the callback - this will post the token back to the parent window
|
|
89
|
+
handleAuthCallback((error) => {
|
|
90
|
+
console.error('Auth callback error:', error);
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
</head>
|
|
94
|
+
<body>
|
|
95
|
+
<div
|
|
96
|
+
style="display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: sans-serif;"
|
|
97
|
+
>
|
|
98
|
+
<p>Completing authentication...</p>
|
|
99
|
+
</div>
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**How it works:**
|
|
105
|
+
|
|
106
|
+
1. User clicks "Login" → SDK opens popup to Prividium user panel
|
|
107
|
+
2. User authenticates → user panel redirects popup to your callback page
|
|
108
|
+
3. Callback page calls `handleAuthCallback()` → token is posted back to parent via `postMessage`
|
|
109
|
+
4. SDK receives token, validates state parameter (CSRF protection), and closes popup
|
|
110
|
+
5. Your app is now authenticated
|
|
111
|
+
|
|
112
|
+
**Note:**
|
|
113
|
+
|
|
114
|
+
- The callback page must be hosted on the same origin as your main application that initiates the auth flow.
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### `createPrividiumChain(config)`
|
|
119
|
+
|
|
120
|
+
Creates a new Prividium SDK instance.
|
|
121
|
+
|
|
122
|
+
**Parameters:**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface PrividiumConfig {
|
|
126
|
+
clientId: string; // OAuth client ID
|
|
127
|
+
chain: Chain; // Viem chain configuration (without rpcUrls)
|
|
128
|
+
rpcUrl: string; // Private RPC endpoint URL
|
|
129
|
+
authBaseUrl: string; // Authorization service base URL
|
|
130
|
+
redirectUrl: string; // OAuth redirect URL
|
|
131
|
+
storage?: Storage; // Custom storage implementation (optional)
|
|
132
|
+
onAuthExpiry?: () => void; // Called when authentication expires (optional)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Returns:** `PrividiumChain`
|
|
137
|
+
|
|
138
|
+
### PrividiumChain Methods
|
|
139
|
+
|
|
140
|
+
#### `authorize(options?)`
|
|
141
|
+
|
|
142
|
+
Opens authentication popup and handles OAuth flow.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
await prividium.authorize({
|
|
146
|
+
popupSize: { w: 600, h: 700 } // Optional custom popup dimensions
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Returns:** `Promise<string>` - JWT token
|
|
151
|
+
|
|
152
|
+
#### `unauthorize()`
|
|
153
|
+
|
|
154
|
+
Clears authentication state and tokens.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
prividium.unauthorize();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### `isAuthorized()`
|
|
161
|
+
|
|
162
|
+
Checks if user is currently authenticated with valid token.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const authenticated = prividium.isAuthorized();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Returns:** `boolean`
|
|
169
|
+
|
|
170
|
+
#### `getAuthHeaders()`
|
|
171
|
+
|
|
172
|
+
Gets current authentication headers for manual use.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const headers = prividium.getAuthHeaders();
|
|
176
|
+
// Returns: { Authorization: 'Bearer <token>' } | null
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Returns:** `Record<string, string> | null`
|
|
180
|
+
|
|
181
|
+
### `handleAuthCallback(onError?)`
|
|
182
|
+
|
|
183
|
+
Handles the OAuth callback on the redirect page. Call this function from your callback page to complete the
|
|
184
|
+
authentication flow.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { handleAuthCallback } from '@repo/prividium-sdk';
|
|
188
|
+
|
|
189
|
+
handleAuthCallback((error) => {
|
|
190
|
+
// Optional: Handle errors (e.g., display error message to user)
|
|
191
|
+
console.error('Auth callback error:', error);
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Parameters:**
|
|
196
|
+
|
|
197
|
+
- `onError?: (error: string) => void` - Optional callback to handle errors
|
|
198
|
+
|
|
199
|
+
**Behavior:**
|
|
200
|
+
|
|
201
|
+
- Extracts token and state from URL hash fragment
|
|
202
|
+
- Posts message to parent window via `postMessage`
|
|
203
|
+
- Automatically closes popup window on success
|
|
204
|
+
- Calls `onError` if any errors occur
|
|
205
|
+
|
|
206
|
+
## Advanced Usage
|
|
207
|
+
|
|
208
|
+
### Custom Storage
|
|
209
|
+
|
|
210
|
+
Implement custom storage for different environments:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
class CustomStorage implements Storage {
|
|
214
|
+
getItem(key: string): string | null {
|
|
215
|
+
// Your implementation
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setItem(key: string, value: string): void {
|
|
219
|
+
// Your implementation
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
removeItem(key: string): void {
|
|
223
|
+
// Your implementation
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const prividium = createPrividiumChain({
|
|
228
|
+
// ... other config
|
|
229
|
+
storage: new CustomStorage()
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Multiple Chains
|
|
234
|
+
|
|
235
|
+
Support multiple Prividium chains:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const testnetPrividium = createPrividiumChain({
|
|
239
|
+
clientId: 'your-testnet-client-id',
|
|
240
|
+
chain: testnetChain,
|
|
241
|
+
rpcUrl: 'https://testnet-rpc.prividium.io',
|
|
242
|
+
authBaseUrl: 'https://testnet-auth.prividium.io',
|
|
243
|
+
redirectUrl: window.location.origin + '/auth/callback'
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const mainnetPrividium = createPrividiumChain({
|
|
247
|
+
clientId: 'your-mainnet-client-id',
|
|
248
|
+
chain: mainnetChain,
|
|
249
|
+
rpcUrl: 'https://mainnet-rpc.prividium.io',
|
|
250
|
+
authBaseUrl: 'https://mainnet-auth.prividium.io',
|
|
251
|
+
redirectUrl: window.location.origin + '/auth/callback'
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Error Handling
|
|
256
|
+
|
|
257
|
+
Handle authentication errors gracefully:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
try {
|
|
261
|
+
await prividium.authorize();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error.message.includes('cancelled')) {
|
|
264
|
+
console.log('User cancelled authentication');
|
|
265
|
+
} else {
|
|
266
|
+
console.error('Authentication failed:', error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Manual HTTP Requests
|
|
272
|
+
|
|
273
|
+
Use authentication headers with custom HTTP requests:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const headers = prividium.getAuthHeaders();
|
|
277
|
+
if (headers) {
|
|
278
|
+
const response = await fetch('/api/protected', {
|
|
279
|
+
headers: {
|
|
280
|
+
...headers,
|
|
281
|
+
'Content-Type': 'application/json'
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Storage Keys
|
|
288
|
+
|
|
289
|
+
The SDK uses the following localStorage keys:
|
|
290
|
+
|
|
291
|
+
- `prividium_jwt_<chainId>` - JWT token storage
|
|
292
|
+
- `prividium_auth_state_<chainId>` - OAuth state parameter
|
|
293
|
+
|
|
294
|
+
## Security Considerations
|
|
295
|
+
|
|
296
|
+
1. **Token Storage**: Tokens are stored in localStorage by default. Consider custom storage for sensitive applications.
|
|
297
|
+
|
|
298
|
+
2. **CSRF Protection**: OAuth state parameter provides CSRF protection during authentication flow.
|
|
299
|
+
|
|
300
|
+
3. **Token Expiration**: SDK automatically validates token expiration and clears expired tokens.
|
|
301
|
+
|
|
302
|
+
4. **Origin Validation**: Popup messages are validated against the configured auth origin.
|
|
303
|
+
|
|
304
|
+
## Development
|
|
305
|
+
|
|
306
|
+
### Building
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npm run build
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Testing
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
npm test
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Linting
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
npm run lint
|
|
322
|
+
npm run lint:fix
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createPrividiumChain } from './prividium-chain.js';
|
|
2
|
+
export type { PrividiumConfig, PrividiumChain, PopupOptions, Storage, TokenData, UserProfile, UserRole } from './types.js';
|
|
3
|
+
export { AUTH_ERRORS, STORAGE_KEYS } from './types.js';
|
|
4
|
+
export { LocalStorage, TokenManager } from './storage.js';
|
|
5
|
+
export { parseToken, isTokenExpired, generateRandomState } from './token-utils.js';
|
|
6
|
+
export { PopupAuth, handleAuthCallback, type AuthCallbackMessage } from './popup-auth.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createPrividiumChain } from './prividium-chain.js';
|
|
2
|
+
export { AUTH_ERRORS, STORAGE_KEYS } from './types.js';
|
|
3
|
+
export { LocalStorage, TokenManager } from './storage.js';
|
|
4
|
+
export { parseToken, isTokenExpired, generateRandomState } from './token-utils.js';
|
|
5
|
+
export { PopupAuth, handleAuthCallback } from './popup-auth.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type PopupOptions } from './types.js';
|
|
2
|
+
import { type TokenManager } from './storage.js';
|
|
3
|
+
export type OauthScope = 'wallet:required';
|
|
4
|
+
export interface PopupAuthConfig {
|
|
5
|
+
authBaseUrl: string;
|
|
6
|
+
clientId: string;
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
tokenManager: TokenManager;
|
|
9
|
+
onAuthExpiry?: () => void;
|
|
10
|
+
scope?: OauthScope[];
|
|
11
|
+
}
|
|
12
|
+
export declare class PopupAuth {
|
|
13
|
+
private config;
|
|
14
|
+
constructor(config: PopupAuthConfig);
|
|
15
|
+
authorize(options?: PopupOptions): Promise<string>;
|
|
16
|
+
private buildAuthUrl;
|
|
17
|
+
private openPopup;
|
|
18
|
+
unauthorize(): void;
|
|
19
|
+
isAuthorized(): boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Interface for auth callback message posted to parent window
|
|
23
|
+
*/
|
|
24
|
+
export interface AuthCallbackMessage {
|
|
25
|
+
type: 'prividium-auth-callback';
|
|
26
|
+
token?: string;
|
|
27
|
+
state?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Handles the authentication callback on the redirect page.
|
|
32
|
+
* This function should be called from the callback page that the user is redirected to
|
|
33
|
+
* after authentication. It extracts the token and state from the URL hash and posts
|
|
34
|
+
* them back to the opener window using postMessage.
|
|
35
|
+
*
|
|
36
|
+
* @param onError - Optional callback to handle errors (e.g., display error message to user)
|
|
37
|
+
*/
|
|
38
|
+
export declare function handleAuthCallback(onError?: (error: string) => void): void;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { AUTH_ERRORS } from './types.js';
|
|
2
|
+
import { generateRandomState } from './token-utils.js';
|
|
3
|
+
export class PopupAuth {
|
|
4
|
+
config;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
async authorize(options = {}) {
|
|
9
|
+
const { popupSize = { w: 500, h: 600 } } = options;
|
|
10
|
+
const state = generateRandomState();
|
|
11
|
+
this.config.tokenManager.setState(state);
|
|
12
|
+
const authUrl = this.buildAuthUrl(state);
|
|
13
|
+
const popup = this.openPopup(authUrl, popupSize);
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
let checkInterval = null;
|
|
17
|
+
let timeoutId = null;
|
|
18
|
+
const cleanup = () => {
|
|
19
|
+
if (checkInterval) {
|
|
20
|
+
clearInterval(checkInterval);
|
|
21
|
+
checkInterval = null;
|
|
22
|
+
}
|
|
23
|
+
if (timeoutId) {
|
|
24
|
+
clearTimeout(timeoutId);
|
|
25
|
+
timeoutId = null;
|
|
26
|
+
}
|
|
27
|
+
window.removeEventListener('message', messageHandler);
|
|
28
|
+
};
|
|
29
|
+
const messageHandler = (event) => {
|
|
30
|
+
// Validate message type
|
|
31
|
+
if (!event.data || event.data.type !== 'prividium-auth-callback') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Validate origin matches redirect URI origin
|
|
35
|
+
const redirectUrl = new URL(this.config.redirectUri);
|
|
36
|
+
if (event.origin !== redirectUrl.origin) {
|
|
37
|
+
console.warn(`Received message from unexpected origin: ${event.origin}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Handle error from callback
|
|
41
|
+
if (event.data.error) {
|
|
42
|
+
popup.close();
|
|
43
|
+
cleanup();
|
|
44
|
+
this.config.tokenManager.clearState();
|
|
45
|
+
this.config.tokenManager.clearToken();
|
|
46
|
+
reject(new Error(event.data.error));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const { token, state: receivedState } = event.data;
|
|
50
|
+
// Validate state
|
|
51
|
+
if (!receivedState) {
|
|
52
|
+
popup.close();
|
|
53
|
+
cleanup();
|
|
54
|
+
this.config.tokenManager.clearState();
|
|
55
|
+
reject(new Error(AUTH_ERRORS.INVALID_STATE));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (receivedState !== state) {
|
|
59
|
+
popup.close();
|
|
60
|
+
cleanup();
|
|
61
|
+
this.config.tokenManager.clearState();
|
|
62
|
+
reject(new Error(AUTH_ERRORS.INVALID_STATE));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Validate token
|
|
66
|
+
if (!token) {
|
|
67
|
+
popup.close();
|
|
68
|
+
cleanup();
|
|
69
|
+
this.config.tokenManager.clearState();
|
|
70
|
+
reject(new Error(AUTH_ERRORS.NO_TOKEN));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Success - store token and resolve
|
|
74
|
+
try {
|
|
75
|
+
const tokenData = this.config.tokenManager.setToken(token);
|
|
76
|
+
this.config.tokenManager.clearState();
|
|
77
|
+
popup.close();
|
|
78
|
+
cleanup();
|
|
79
|
+
resolve(tokenData.rawToken);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
popup.close();
|
|
83
|
+
cleanup();
|
|
84
|
+
this.config.tokenManager.clearState();
|
|
85
|
+
this.config.tokenManager.clearToken();
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
|
87
|
+
reject(error);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
// Listen for postMessage from callback page
|
|
91
|
+
window.addEventListener('message', messageHandler);
|
|
92
|
+
// Check if popup is closed (user cancelled)
|
|
93
|
+
checkInterval = setInterval(() => {
|
|
94
|
+
if (popup.closed) {
|
|
95
|
+
cleanup();
|
|
96
|
+
this.config.tokenManager.clearState();
|
|
97
|
+
reject(new Error('Authentication was cancelled'));
|
|
98
|
+
}
|
|
99
|
+
}, 500);
|
|
100
|
+
// Set timeout for authentication
|
|
101
|
+
timeoutId = setTimeout(() => {
|
|
102
|
+
if (!popup.closed) {
|
|
103
|
+
popup.close();
|
|
104
|
+
}
|
|
105
|
+
cleanup();
|
|
106
|
+
this.config.tokenManager.clearState();
|
|
107
|
+
reject(new Error('Authentication timeout'));
|
|
108
|
+
}, TIMEOUT_MS);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
buildAuthUrl(state) {
|
|
112
|
+
const url = new URL('/auth/authorize', this.config.authBaseUrl);
|
|
113
|
+
url.searchParams.set('client_id', this.config.clientId);
|
|
114
|
+
url.searchParams.set('redirect_uri', this.config.redirectUri);
|
|
115
|
+
url.searchParams.set('state', state);
|
|
116
|
+
url.searchParams.set('response_type', 'token');
|
|
117
|
+
if (this.config.scope?.length) {
|
|
118
|
+
for (const scope of this.config.scope) {
|
|
119
|
+
url.searchParams.append('scope', scope);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return url.toString();
|
|
123
|
+
}
|
|
124
|
+
openPopup(url, size) {
|
|
125
|
+
const left = window.screen.width / 2 - size.w / 2;
|
|
126
|
+
const top = window.screen.height / 2 - size.h / 2;
|
|
127
|
+
// Note: We intentionally do NOT use 'noopener' here because:
|
|
128
|
+
// 1. The callback page needs window.opener to post messages back
|
|
129
|
+
// 2. We validate message origin strictly in the message handler
|
|
130
|
+
// 3. The auth flow goes through trusted domains (user-panel -> callback page)
|
|
131
|
+
// This is the standard approach for OAuth2 popup flows
|
|
132
|
+
const popup = window.open(url, 'prividium-auth', `scrollbars=yes,resizable=yes,status=yes,location=yes,toolbar=no,menubar=no,width=${size.w},height=${size.h},top=${top},left=${left}`);
|
|
133
|
+
if (!popup) {
|
|
134
|
+
throw new Error('Failed to open popup. Please allow popups for this site.');
|
|
135
|
+
}
|
|
136
|
+
return popup;
|
|
137
|
+
}
|
|
138
|
+
unauthorize() {
|
|
139
|
+
this.config.tokenManager.clearToken();
|
|
140
|
+
this.config.tokenManager.clearState();
|
|
141
|
+
}
|
|
142
|
+
isAuthorized() {
|
|
143
|
+
return this.config.tokenManager.isAuthorized();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Handles the authentication callback on the redirect page.
|
|
148
|
+
* This function should be called from the callback page that the user is redirected to
|
|
149
|
+
* after authentication. It extracts the token and state from the URL hash and posts
|
|
150
|
+
* them back to the opener window using postMessage.
|
|
151
|
+
*
|
|
152
|
+
* @param onError - Optional callback to handle errors (e.g., display error message to user)
|
|
153
|
+
*/
|
|
154
|
+
export function handleAuthCallback(onError) {
|
|
155
|
+
try {
|
|
156
|
+
// Check if window.opener exists
|
|
157
|
+
if (!window.opener) {
|
|
158
|
+
const error = 'No opener window found. This page must be opened from the authentication popup.';
|
|
159
|
+
onError?.(error);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Post only to current for secure postMessage
|
|
163
|
+
const origin = window.origin;
|
|
164
|
+
// Parse hash parameters
|
|
165
|
+
const hash = window.location.hash.replace(/^#/, '');
|
|
166
|
+
const params = new URLSearchParams(hash);
|
|
167
|
+
const token = params.get('token');
|
|
168
|
+
const state = params.get('state');
|
|
169
|
+
if (!token) {
|
|
170
|
+
const message = {
|
|
171
|
+
type: 'prividium-auth-callback',
|
|
172
|
+
error: AUTH_ERRORS.NO_TOKEN
|
|
173
|
+
};
|
|
174
|
+
window.opener.postMessage(message, origin);
|
|
175
|
+
onError?.(AUTH_ERRORS.NO_TOKEN);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!state) {
|
|
179
|
+
const message = {
|
|
180
|
+
type: 'prividium-auth-callback',
|
|
181
|
+
error: AUTH_ERRORS.NO_RECEIVED_STATE
|
|
182
|
+
};
|
|
183
|
+
window.opener.postMessage(message, origin);
|
|
184
|
+
onError?.(AUTH_ERRORS.NO_RECEIVED_STATE);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Post success message to opener
|
|
188
|
+
const message = {
|
|
189
|
+
type: 'prividium-auth-callback',
|
|
190
|
+
token: decodeURIComponent(token),
|
|
191
|
+
state
|
|
192
|
+
};
|
|
193
|
+
window.opener.postMessage(message, origin);
|
|
194
|
+
// Close the window after a short delay to ensure message is sent
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
window.close();
|
|
197
|
+
}, 100);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
201
|
+
onError?.(errorMessage);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { http } from 'viem';
|
|
2
|
+
import { chainConfig } from 'viem/zksync';
|
|
3
|
+
import { LocalStorage, TokenManager } from './storage.js';
|
|
4
|
+
import { PopupAuth } from './popup-auth.js';
|
|
5
|
+
export function createPrividiumChain(config) {
|
|
6
|
+
const storage = config.storage || new LocalStorage();
|
|
7
|
+
const tokenManager = new TokenManager(storage, config.chain.id);
|
|
8
|
+
const popupAuth = new PopupAuth({
|
|
9
|
+
clientId: config.clientId,
|
|
10
|
+
authBaseUrl: config.authBaseUrl,
|
|
11
|
+
redirectUri: config.redirectUrl,
|
|
12
|
+
scope: config.scope,
|
|
13
|
+
tokenManager,
|
|
14
|
+
onAuthExpiry: config.onAuthExpiry
|
|
15
|
+
});
|
|
16
|
+
const getAuthHeaders = () => {
|
|
17
|
+
const token = tokenManager.getToken();
|
|
18
|
+
if (token) {
|
|
19
|
+
return {
|
|
20
|
+
Authorization: `Bearer ${token.rawToken}`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
// Create transport with auth integration using viem callbacks
|
|
26
|
+
const transport = http(config.rpcUrl, {
|
|
27
|
+
batch: false,
|
|
28
|
+
fetchOptions: {
|
|
29
|
+
headers: getAuthHeaders() || {}
|
|
30
|
+
},
|
|
31
|
+
onFetchRequest(_request, init) {
|
|
32
|
+
init.headers = {
|
|
33
|
+
...init.headers,
|
|
34
|
+
...getAuthHeaders()
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
onFetchResponse: (response) => {
|
|
38
|
+
// Handle 403 responses (expired token or no access)
|
|
39
|
+
if (response.status === 403) {
|
|
40
|
+
tokenManager.clearToken();
|
|
41
|
+
config.onAuthExpiry?.();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
chain: {
|
|
47
|
+
...chainConfig,
|
|
48
|
+
...config.chain,
|
|
49
|
+
contracts: {
|
|
50
|
+
...chainConfig.contracts,
|
|
51
|
+
...config.chain.contracts,
|
|
52
|
+
multicall3: undefined // Prividium doesn't support multicall yet
|
|
53
|
+
},
|
|
54
|
+
rpcUrls: { default: { http: [config.rpcUrl] } }
|
|
55
|
+
},
|
|
56
|
+
transport,
|
|
57
|
+
async authorize(options) {
|
|
58
|
+
return popupAuth.authorize(options);
|
|
59
|
+
},
|
|
60
|
+
unauthorize() {
|
|
61
|
+
popupAuth.unauthorize();
|
|
62
|
+
},
|
|
63
|
+
isAuthorized() {
|
|
64
|
+
return popupAuth.isAuthorized();
|
|
65
|
+
},
|
|
66
|
+
getAuthHeaders,
|
|
67
|
+
async fetchUser() {
|
|
68
|
+
const headers = getAuthHeaders();
|
|
69
|
+
if (!headers) {
|
|
70
|
+
throw new Error('Authentication required. Please call authorize() first.');
|
|
71
|
+
}
|
|
72
|
+
const response = await fetch(`${config.permissionsApiBaseUrl}/api/profiles/me`, {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
...headers
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
if (response.status === 403) {
|
|
80
|
+
tokenManager.clearToken();
|
|
81
|
+
config.onAuthExpiry?.();
|
|
82
|
+
throw new Error('Authentication required. Please call authorize() first.');
|
|
83
|
+
}
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`Failed to fetch user profile: ${response.status} ${response.statusText}`);
|
|
86
|
+
}
|
|
87
|
+
const userData = (await response.json());
|
|
88
|
+
return {
|
|
89
|
+
userId: userData.userId,
|
|
90
|
+
createdAt: new Date(userData.createdAt),
|
|
91
|
+
displayName: userData.displayName,
|
|
92
|
+
updatedAt: new Date(userData.updatedAt),
|
|
93
|
+
roles: userData.roles,
|
|
94
|
+
walletAddresses: userData.walletAddresses
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Storage, type TokenData } from './types.js';
|
|
2
|
+
export declare class LocalStorage implements Storage {
|
|
3
|
+
getItem(key: string): string | null;
|
|
4
|
+
setItem(key: string, value: string): void;
|
|
5
|
+
removeItem(key: string): void;
|
|
6
|
+
}
|
|
7
|
+
export declare class TokenManager {
|
|
8
|
+
private storage;
|
|
9
|
+
private chainId;
|
|
10
|
+
private tokenCache;
|
|
11
|
+
constructor(storage: Storage, chainId: number);
|
|
12
|
+
private get tokenKey();
|
|
13
|
+
private get stateKey();
|
|
14
|
+
getToken(): TokenData | null;
|
|
15
|
+
setToken(rawToken: string): TokenData;
|
|
16
|
+
clearToken(): void;
|
|
17
|
+
isAuthorized(): boolean;
|
|
18
|
+
setState(state: string): void;
|
|
19
|
+
getState(): string | null;
|
|
20
|
+
clearState(): void;
|
|
21
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { STORAGE_KEYS } from './types.js';
|
|
2
|
+
import { parseToken, isTokenExpired } from './token-utils.js';
|
|
3
|
+
export class LocalStorage {
|
|
4
|
+
getItem(key) {
|
|
5
|
+
if (typeof localStorage === 'undefined') {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return localStorage.getItem(key);
|
|
9
|
+
}
|
|
10
|
+
setItem(key, value) {
|
|
11
|
+
if (typeof localStorage !== 'undefined') {
|
|
12
|
+
localStorage.setItem(key, value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
removeItem(key) {
|
|
16
|
+
if (typeof localStorage !== 'undefined') {
|
|
17
|
+
localStorage.removeItem(key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class TokenManager {
|
|
22
|
+
storage;
|
|
23
|
+
chainId;
|
|
24
|
+
tokenCache = null;
|
|
25
|
+
constructor(storage, chainId) {
|
|
26
|
+
this.storage = storage;
|
|
27
|
+
this.chainId = chainId;
|
|
28
|
+
}
|
|
29
|
+
get tokenKey() {
|
|
30
|
+
return `${STORAGE_KEYS.TOKEN_PREFIX}${this.chainId}`;
|
|
31
|
+
}
|
|
32
|
+
get stateKey() {
|
|
33
|
+
return `${STORAGE_KEYS.STATE_PREFIX}${this.chainId}`;
|
|
34
|
+
}
|
|
35
|
+
getToken() {
|
|
36
|
+
if (this.tokenCache && !isTokenExpired(this.tokenCache)) {
|
|
37
|
+
return this.tokenCache;
|
|
38
|
+
}
|
|
39
|
+
const rawToken = this.storage.getItem(this.tokenKey);
|
|
40
|
+
if (!rawToken) {
|
|
41
|
+
this.tokenCache = null;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
this.tokenCache = parseToken(rawToken);
|
|
46
|
+
return this.tokenCache;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
this.clearToken();
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
setToken(rawToken) {
|
|
54
|
+
try {
|
|
55
|
+
const tokenData = parseToken(rawToken);
|
|
56
|
+
this.storage.setItem(this.tokenKey, rawToken);
|
|
57
|
+
this.tokenCache = tokenData;
|
|
58
|
+
return tokenData;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.clearToken();
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
clearToken() {
|
|
66
|
+
this.storage.removeItem(this.tokenKey);
|
|
67
|
+
this.tokenCache = null;
|
|
68
|
+
}
|
|
69
|
+
isAuthorized() {
|
|
70
|
+
const token = this.getToken();
|
|
71
|
+
return token !== null && !isTokenExpired(token);
|
|
72
|
+
}
|
|
73
|
+
setState(state) {
|
|
74
|
+
this.storage.setItem(this.stateKey, state);
|
|
75
|
+
}
|
|
76
|
+
getState() {
|
|
77
|
+
return this.storage.getItem(this.stateKey);
|
|
78
|
+
}
|
|
79
|
+
clearState() {
|
|
80
|
+
this.storage.removeItem(this.stateKey);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AUTH_ERRORS } from './types.js';
|
|
3
|
+
const tokenSchema = z.object({
|
|
4
|
+
exp: z.coerce.number().int(),
|
|
5
|
+
sub: z.string().min(1),
|
|
6
|
+
preferred_username: z.string().optional()
|
|
7
|
+
});
|
|
8
|
+
function base64UrlDecode(str) {
|
|
9
|
+
const base64Encoded = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
10
|
+
const padding = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4));
|
|
11
|
+
const base64WithPadding = base64Encoded + padding;
|
|
12
|
+
return atob(base64WithPadding)
|
|
13
|
+
.split('')
|
|
14
|
+
.map((char) => String.fromCharCode(char.charCodeAt(0)))
|
|
15
|
+
.join('');
|
|
16
|
+
}
|
|
17
|
+
export function parseToken(rawToken) {
|
|
18
|
+
const parts = rawToken.split('.');
|
|
19
|
+
if (parts.length < 3) {
|
|
20
|
+
throw new Error(AUTH_ERRORS.INVALID_JWT);
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
25
|
+
parsed = JSON.parse(base64UrlDecode(parts[1]));
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if (!(e instanceof SyntaxError)) {
|
|
29
|
+
console.error(e);
|
|
30
|
+
}
|
|
31
|
+
throw new Error(AUTH_ERRORS.INVALID_JWT);
|
|
32
|
+
}
|
|
33
|
+
const validated = tokenSchema.safeParse(parsed);
|
|
34
|
+
if (!validated.success) {
|
|
35
|
+
throw new Error(`Invalid JWT body format: ${validated.error.message}`);
|
|
36
|
+
}
|
|
37
|
+
const expirationDate = new Date(validated.data.exp * 1000);
|
|
38
|
+
if (expirationDate <= new Date()) {
|
|
39
|
+
throw new Error(AUTH_ERRORS.EXPIRED_TOKEN);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
rawToken,
|
|
43
|
+
expirationDate,
|
|
44
|
+
sub: validated.data.sub,
|
|
45
|
+
preferred_username: validated.data.preferred_username
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function isTokenExpired(tokenData) {
|
|
49
|
+
return tokenData.expirationDate <= new Date();
|
|
50
|
+
}
|
|
51
|
+
export function generateRandomState() {
|
|
52
|
+
const array = new Uint8Array(32);
|
|
53
|
+
crypto.getRandomValues(array);
|
|
54
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
55
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Chain, type Transport } from 'viem';
|
|
2
|
+
import { type OauthScope } from './popup-auth.js';
|
|
3
|
+
export interface Storage {
|
|
4
|
+
getItem(key: string): string | null;
|
|
5
|
+
setItem(key: string, value: string): void;
|
|
6
|
+
removeItem(key: string): void;
|
|
7
|
+
}
|
|
8
|
+
export interface PrividiumConfig {
|
|
9
|
+
clientId: string;
|
|
10
|
+
chain: Omit<Chain, 'rpcUrls'>;
|
|
11
|
+
rpcUrl: string;
|
|
12
|
+
authBaseUrl: string;
|
|
13
|
+
redirectUrl: string;
|
|
14
|
+
permissionsApiBaseUrl: string;
|
|
15
|
+
scope?: OauthScope[];
|
|
16
|
+
storage?: Storage;
|
|
17
|
+
onAuthExpiry?: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface UserRole {
|
|
20
|
+
roleName: string;
|
|
21
|
+
}
|
|
22
|
+
export interface UserProfile {
|
|
23
|
+
userId: string;
|
|
24
|
+
createdAt: Date;
|
|
25
|
+
displayName: string | null;
|
|
26
|
+
updatedAt: Date;
|
|
27
|
+
roles: UserRole[];
|
|
28
|
+
walletAddresses: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface PrividiumChain {
|
|
31
|
+
chain: Chain;
|
|
32
|
+
transport: Transport;
|
|
33
|
+
authorize(opts?: {
|
|
34
|
+
popupSize?: {
|
|
35
|
+
w: number;
|
|
36
|
+
h: number;
|
|
37
|
+
};
|
|
38
|
+
}): Promise<string>;
|
|
39
|
+
unauthorize(): void;
|
|
40
|
+
isAuthorized(): boolean;
|
|
41
|
+
getAuthHeaders(): Record<string, string> | null;
|
|
42
|
+
fetchUser(): Promise<UserProfile>;
|
|
43
|
+
}
|
|
44
|
+
export interface TokenData {
|
|
45
|
+
rawToken: string;
|
|
46
|
+
expirationDate: Date;
|
|
47
|
+
sub: string;
|
|
48
|
+
preferred_username?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface PopupOptions {
|
|
51
|
+
popupSize?: {
|
|
52
|
+
w: number;
|
|
53
|
+
h: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export declare const AUTH_ERRORS: {
|
|
57
|
+
readonly INVALID_STATE: "Invalid state parameter";
|
|
58
|
+
readonly NO_RECEIVED_STATE: "No state parameter";
|
|
59
|
+
readonly NO_SAVED_STATE: "No saved state";
|
|
60
|
+
readonly NO_TOKEN: "No token received";
|
|
61
|
+
readonly EXPIRED_TOKEN: "Expired token";
|
|
62
|
+
readonly INVALID_JWT: "Invalid JWT format";
|
|
63
|
+
readonly AUTH_REQUIRED: "Authentication required";
|
|
64
|
+
};
|
|
65
|
+
export declare const STORAGE_KEYS: {
|
|
66
|
+
readonly STATE_PREFIX: "prividium_auth_state_";
|
|
67
|
+
readonly TOKEN_PREFIX: "prividium_jwt_";
|
|
68
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const AUTH_ERRORS = {
|
|
2
|
+
INVALID_STATE: 'Invalid state parameter',
|
|
3
|
+
NO_RECEIVED_STATE: 'No state parameter',
|
|
4
|
+
NO_SAVED_STATE: 'No saved state',
|
|
5
|
+
NO_TOKEN: 'No token received',
|
|
6
|
+
EXPIRED_TOKEN: 'Expired token',
|
|
7
|
+
INVALID_JWT: 'Invalid JWT format',
|
|
8
|
+
AUTH_REQUIRED: 'Authentication required'
|
|
9
|
+
};
|
|
10
|
+
export const STORAGE_KEYS = {
|
|
11
|
+
STATE_PREFIX: 'prividium_auth_state_',
|
|
12
|
+
TOKEN_PREFIX: 'prividium_jwt_'
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prividium",
|
|
3
|
+
"version": "0.0.1-beta",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"import": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"lint": "eslint . --ignore-path ../../.gitignore --max-warnings 0",
|
|
16
|
+
"lint:fix": "eslint . --fix --ignore-path ../../.gitignore",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"typecheck:test": "tsc --project tsconfig.test.json --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"zod": "^3.23.8"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"viem": ">=2.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@repo/eslint-config": "workspace:*",
|
|
30
|
+
"@types/node": "^22.8.6",
|
|
31
|
+
"eslint": "^8",
|
|
32
|
+
"jsdom": "^25.0.0",
|
|
33
|
+
"typescript": "^5.8.3",
|
|
34
|
+
"vitest": "^3.2.4"
|
|
35
|
+
}
|
|
36
|
+
}
|