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 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
@@ -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,2 @@
1
+ import { type PrividiumConfig, type PrividiumChain } from './types.js';
2
+ export declare function createPrividiumChain(config: PrividiumConfig): PrividiumChain;
@@ -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
+ }
@@ -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,4 @@
1
+ import { type TokenData } from './types.js';
2
+ export declare function parseToken(rawToken: string): TokenData;
3
+ export declare function isTokenExpired(tokenData: TokenData): boolean;
4
+ export declare function generateRandomState(): string;
@@ -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
+ }
@@ -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
+ }