prividium 0.0.1-beta → 0.0.3-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Prividium SDK
2
2
 
3
- A TypeScript SDK for integrating with Prividium's authorization system, providing seamless authentication and secure RPC
4
- communication for blockchain applications.
3
+ A TypeScript SDK for integrating with the Prividium authorization system, providing seamless authentication and secure
4
+ RPC communication for blockchain applications.
5
5
 
6
6
  ## Features
7
7
 
@@ -33,6 +33,8 @@ const prividiumChain = defineChain({
33
33
  });
34
34
 
35
35
  // Create SDK instance
36
+ // Note: replace the URLs and clientId with your actual values
37
+ // Make sure clientId is registered in User Panel (OAUTH_CLIENTS)
36
38
  const prividium = createPrividiumChain({
37
39
  clientId: 'your-client-id',
38
40
  chain: prividiumChain,
@@ -70,7 +72,60 @@ const balance = await client.getBalance({
70
72
  });
71
73
  ```
72
74
 
73
- ### 4. Setup OAuth Callback Page
75
+ ### 4. Sending Transactions with Injected Wallets (MetaMask)
76
+
77
+ Before sending transactions through injected wallets, you need to add the Prividium network and enable wallet RPC for
78
+ each transaction.
79
+
80
+ ```typescript
81
+ import { createWalletClient, custom, encodeFunctionData } from 'viem';
82
+
83
+ // Add Prividium network to MetaMask
84
+ await prividium.addNetworkToWallet();
85
+
86
+ // Create wallet client for MetaMask
87
+ const walletClient = createWalletClient({
88
+ chain: prividium.chain,
89
+ transport: custom(window.ethereum)
90
+ });
91
+
92
+ const [address] = await walletClient.getAddresses();
93
+
94
+ // Prepare transaction with viem
95
+ const greeterContract = '0x...';
96
+ const request = await walletClient.prepareTransactionRequest({
97
+ account: address,
98
+ to: greeterContract,
99
+ data: encodeFunctionData({
100
+ abi: [
101
+ {
102
+ name: 'setGreeting',
103
+ type: 'function',
104
+ inputs: [{ name: 'greeting', type: 'string' }],
105
+ outputs: []
106
+ }
107
+ ],
108
+ functionName: 'setGreeting',
109
+ args: ['Hello, Prividium!']
110
+ })
111
+ });
112
+
113
+ // Enable wallet token for this transaction
114
+ await prividium.enableWalletToken({
115
+ walletAddress: address,
116
+ contractAddress: greeterContract,
117
+ nonce: Number(request.nonce),
118
+ calldata: request.data
119
+ });
120
+
121
+ // Send transaction through MetaMask
122
+ const hash = await walletClient.sendTransaction(request);
123
+ ```
124
+
125
+ **Note:** Call `enableWalletToken(...)` before each transaction. Permission is transaction-specific and expires after 1
126
+ hour.
127
+
128
+ ### 5. Setup OAuth Callback Page
74
129
 
75
130
  The SDK requires a callback page to complete the authentication flow securely using `postMessage`. Create a callback
76
131
  page at the `redirectUrl` you configured:
@@ -113,6 +168,34 @@ page at the `redirectUrl` you configured:
113
168
 
114
169
  - The callback page must be hosted on the same origin as your main application that initiates the auth flow.
115
170
 
171
+ ## OAuth Scopes
172
+
173
+ The SDK supports requesting specific OAuth scopes during authorization to ensure users meet certain requirements:
174
+
175
+ ```typescript
176
+ import { createPrividiumChain, type OauthScope } from 'prividium';
177
+
178
+ const prividium = createPrividiumChain({
179
+ clientId: 'your-client-id',
180
+ chain: prividiumChain,
181
+ rpcUrl: 'https://rpc.prividium.io',
182
+ authBaseUrl: 'https://auth.prividium.io',
183
+ redirectUrl: window.location.origin + '/auth/callback',
184
+ scope: ['wallet:required', 'network:required'], // Request specific scopes
185
+ onAuthExpiry: () => {
186
+ console.log('Authentication expired');
187
+ }
188
+ });
189
+ ```
190
+
191
+ ### Available Scopes
192
+
193
+ - **`wallet:required`** - Ensures the user has at least one wallet address associated with their account
194
+ - **`network:required`** - Ensures the user has a wallet connected with the correct chain configuration
195
+
196
+ When scopes are specified, the authorization flow will validate that the user meets all requirements. If requirements
197
+ are not met, the user panel will guide them through the necessary setup steps before completing authentication.
198
+
116
199
  ## API Reference
117
200
 
118
201
  ### `createPrividiumChain(config)`
@@ -128,6 +211,7 @@ interface PrividiumConfig {
128
211
  rpcUrl: string; // Private RPC endpoint URL
129
212
  authBaseUrl: string; // Authorization service base URL
130
213
  redirectUrl: string; // OAuth redirect URL
214
+ scope?: OauthScope[]; // Optional OAuth scopes to request (optional)
131
215
  storage?: Storage; // Custom storage implementation (optional)
132
216
  onAuthExpiry?: () => void; // Called when authentication expires (optional)
133
217
  }
package/bin/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli/index.js';
@@ -0,0 +1,9 @@
1
+ import yargs from 'yargs';
2
+ export const BASE_CLI = yargs(process.argv.slice(2))
3
+ .scriptName('prividium-cli')
4
+ .option('configPath', {
5
+ alias: ['c', 'config-path', 'config'],
6
+ description: 'Path for config file. By default config file is stored under user personal folder',
7
+ type: 'string',
8
+ demandOption: false
9
+ });
@@ -0,0 +1,40 @@
1
+ import { ConfigFile } from '../server/config-file.js';
2
+ function clearConfig(path) {
3
+ const file = new ConfigFile(path);
4
+ file.remove();
5
+ }
6
+ function configPath(path) {
7
+ const file = new ConfigFile(path);
8
+ file.printPath();
9
+ }
10
+ function printConfig(path) {
11
+ const file = new ConfigFile(path);
12
+ file.print();
13
+ }
14
+ function updateConfig(urls, path) {
15
+ const file = new ConfigFile(path);
16
+ file.write(urls);
17
+ }
18
+ export const addConfig = (cli) => {
19
+ return cli.command('config', 'Manipulate local configuration file', (yargs) => yargs
20
+ .command('clear', 'Clears current configuration file', (yargs) => yargs, (args) => clearConfig(args.configPath))
21
+ .command('path', 'Shows local config file path', (yargs) => yargs, (args) => configPath(args.configPath))
22
+ .command('print', 'Prints current configuration', (yargs) => yargs, (args) => printConfig(args.configPath))
23
+ .command('set', 'Updates config', (yargs) => yargs
24
+ .option('rpcUrl', {
25
+ alias: ['rpc-url', 'r'],
26
+ description: 'Specifies target Prividium rpc url. These takes precedence over config file and env variable.',
27
+ demandOption: true,
28
+ type: 'string'
29
+ })
30
+ .option('userPanelUrl', {
31
+ alias: ['user-panel-url', 'u'],
32
+ description: 'Specifies url used to log in into the Prividium network. Takes precedence over config file and env variable',
33
+ type: 'string',
34
+ demandOption: true
35
+ }), (args) => updateConfig({
36
+ prividiumRpcUrl: args.rpcUrl,
37
+ userPanelUrl: args.userPanelUrl
38
+ }, args.configPath))
39
+ .demandCommand());
40
+ };
@@ -0,0 +1,71 @@
1
+ import { buildServer } from '../server/server.js';
2
+ import { CreationWorkflow } from '../server/connection-workflow.js';
3
+ import { ConfigFile } from '../server/config-file.js';
4
+ import { z } from 'zod';
5
+ const envSchema = z.object({
6
+ PRIVIDIUM_RPC_URL: z.string().optional(),
7
+ USER_PANEL_URL: z.string().optional()
8
+ });
9
+ async function startServer(opts) {
10
+ const config = new ConfigFile(opts.configPath);
11
+ const workflow = new CreationWorkflow(config);
12
+ workflow.start();
13
+ const configConfig = config.read();
14
+ const env = envSchema.parse(process.env);
15
+ const givenRpcUrl = opts.rpcUrl ?? env.PRIVIDIUM_RPC_URL ?? configConfig.prividiumRpcUrl;
16
+ const givenUserPanelUrl = opts.userPanelUrl ?? env.USER_PANEL_URL ?? configConfig.userPanelUrl;
17
+ const { prividiumRpcUrl, userPanelUrl } = await workflow.gatherData(givenRpcUrl, givenUserPanelUrl);
18
+ const app = buildServer({
19
+ prividiumRpcUrl,
20
+ userPanelUrl,
21
+ async onSubmit() {
22
+ await workflow.onSubmit();
23
+ },
24
+ onCall(methodName) {
25
+ workflow.onMessage(methodName);
26
+ }
27
+ });
28
+ const serverUrl = await app.listen({
29
+ port: 24101,
30
+ host: '127.0.0.1'
31
+ });
32
+ await workflow.waitForAuthentication(serverUrl);
33
+ }
34
+ export const addProxy = (cli) => {
35
+ return cli.command('proxy', 'Starts authenticated rpc proxy server', (yargs) => yargs
36
+ .option('rpcUrl', {
37
+ alias: ['rpc-url', 'r'],
38
+ description: 'Specifies target Prividium rpc url. These takes precedence over config file and env variable.',
39
+ demandOption: false,
40
+ type: 'string'
41
+ })
42
+ .option('userPanelUrl', {
43
+ alias: ['user-panel-url', 'u'],
44
+ description: 'Specifies url used to log in into the Prividium network. Takes precedence over config file and env variable',
45
+ type: 'string'
46
+ })
47
+ .option('configPath', {
48
+ alias: ['c', 'config-path', 'config'],
49
+ description: 'Path for config file. By default config file is stored under user personal folder',
50
+ type: 'string',
51
+ demandOption: false
52
+ })
53
+ .option('port', {
54
+ alias: ['p'],
55
+ description: 'Port used for local proxy. This has to match with the port configured in your Prividium network.',
56
+ default: 24101,
57
+ type: 'number'
58
+ })
59
+ .option('host', {
60
+ alias: 'h',
61
+ description: 'Host used for local server. By default traffic from outside localhost is disabled.',
62
+ default: '127.0.0.1',
63
+ type: 'string'
64
+ }), (args) => startServer({
65
+ rpcUrl: args.rpcUrl,
66
+ userPanelUrl: args.userPanelUrl,
67
+ configPath: args.configPath,
68
+ port: args.port,
69
+ host: args.host
70
+ }));
71
+ };
@@ -0,0 +1,12 @@
1
+ import { BASE_CLI } from './base-cli.js';
2
+ import { addProxy } from './commands/proxy.js';
3
+ import { addConfig } from './commands/config.js';
4
+ const actions = [addProxy, addConfig];
5
+ const baseCli = actions.reduce((argv, applyAction) => {
6
+ return applyAction(argv);
7
+ }, BASE_CLI);
8
+ const cli = baseCli.help().strict().demandCommand();
9
+ cli.parseAsync().catch((e) => {
10
+ console.error(e);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,58 @@
1
+ import appDirs from 'appdirsjs';
2
+ import path from 'node:path';
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { z } from 'zod';
5
+ import { log } from '@clack/prompts';
6
+ import color from 'kleur';
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
8
+ const appDirsFn = appDirs.default;
9
+ const dirs = appDirsFn({ appName: 'prividium-proxy' });
10
+ const urlConfigSchema = z.object({
11
+ prividiumRpcUrl: z.string(),
12
+ userPanelUrl: z.string()
13
+ });
14
+ export class ConfigFile {
15
+ filePath;
16
+ constructor(filePath) {
17
+ if (filePath) {
18
+ this.filePath = filePath;
19
+ }
20
+ else {
21
+ this.filePath = path.join(dirs.config, 'config.json');
22
+ }
23
+ }
24
+ read() {
25
+ if (!existsSync(this.filePath)) {
26
+ return {};
27
+ }
28
+ else {
29
+ const data = readFileSync(this.filePath).toString();
30
+ try {
31
+ return urlConfigSchema.parse(JSON.parse(data));
32
+ }
33
+ catch {
34
+ rmSync(this.filePath);
35
+ log.warn('Corrupted configuration file');
36
+ return {};
37
+ }
38
+ }
39
+ }
40
+ write(param) {
41
+ const dir = path.parse(this.filePath).dir;
42
+ mkdirSync(dir, { recursive: true });
43
+ writeFileSync(this.filePath, JSON.stringify(param));
44
+ }
45
+ remove() {
46
+ if (existsSync(this.filePath)) {
47
+ rmSync(this.filePath);
48
+ }
49
+ }
50
+ print() {
51
+ const { prividiumRpcUrl, userPanelUrl } = this.read();
52
+ console.log(`${color.bold('Prividium rpc url')}: ${prividiumRpcUrl}`);
53
+ console.log(`${color.bold('Log in url')}: ${userPanelUrl}`);
54
+ }
55
+ printPath() {
56
+ console.log(this.filePath);
57
+ }
58
+ }
@@ -0,0 +1,93 @@
1
+ import { confirm, intro, log, tasks, text } from '@clack/prompts';
2
+ import color from 'kleur';
3
+ import { z } from 'zod';
4
+ import { setTimeout } from 'timers/promises';
5
+ const HEADER = ` .__ .__ .___.__ .__ .__
6
+ _____________|__|__ _|__| __| _/|__|__ __ _____ ____ | | |__|
7
+ \\____ \\_ __ \\ \\ \\/ / |/ __ | | | | \\/ \\ ______ _/ ___\\| | | |
8
+ | |_> > | \\/ |\\ /| / /_/ | | | | / Y Y \\ /_____/ \\ \\___| |_| |
9
+ | __/|__| |__| \\_/ |__\\____ | |__|____/|__|_| / \\___ >____/__|
10
+ |__| \\/ \\/ \\/ `;
11
+ export class CreationWorkflow {
12
+ config;
13
+ submitCallback;
14
+ constructor(config) {
15
+ this.config = config;
16
+ this.submitCallback = undefined;
17
+ }
18
+ start() {
19
+ console.log(color.blue(HEADER));
20
+ console.log('');
21
+ intro('Starting prividium proxy');
22
+ }
23
+ async gatherData(maybePrividiumRpcUrl, maybeUserPanelUrl) {
24
+ const { url: prividiumRpcUrl, prompted: prompted1 } = await this.askForUrl(maybePrividiumRpcUrl, 'prividium rpc');
25
+ const { url: userPanelUrl, prompted: prompted2 } = await this.askForUrl(maybeUserPanelUrl, 'user panel');
26
+ if (prompted1 || prompted2) {
27
+ const confirmation = await confirm({
28
+ message: 'Do wou want to store this config for future usages?'
29
+ });
30
+ if (confirmation) {
31
+ this.config.write({ prividiumRpcUrl, userPanelUrl });
32
+ }
33
+ }
34
+ return {
35
+ prividiumRpcUrl,
36
+ userPanelUrl
37
+ };
38
+ }
39
+ async askForUrl(maybeUrl, name) {
40
+ if (maybeUrl !== undefined) {
41
+ const res = z.url().safeParse(maybeUrl);
42
+ if (!res.success) {
43
+ log.error(`Invalidad ${name} url provided: ${maybeUrl}`);
44
+ process.exit(1);
45
+ }
46
+ log.info(`Using ${name} url: ${color.bold(maybeUrl)}`);
47
+ return { url: maybeUrl, prompted: false };
48
+ }
49
+ else {
50
+ const url = await text({
51
+ message: `Please insert your ${name} url`,
52
+ validate(value) {
53
+ const parsed = z.url().safeParse(value);
54
+ if (!parsed.success) {
55
+ return 'Please provide a valid url';
56
+ }
57
+ }
58
+ });
59
+ if (typeof url === 'symbol') {
60
+ log.warn('Canceled by the user');
61
+ process.exit(1);
62
+ }
63
+ return { url, prompted: true };
64
+ }
65
+ }
66
+ async waitForAuthentication(url) {
67
+ log.info(`Please log in: ${url}`);
68
+ await tasks([
69
+ {
70
+ task: async () => {
71
+ return new Promise((resolve) => {
72
+ this.submitCallback = resolve;
73
+ });
74
+ },
75
+ title: 'Waiting for authentication'
76
+ }
77
+ ]);
78
+ }
79
+ async onSubmit() {
80
+ if (this.submitCallback === undefined) {
81
+ throw new Error('Missing submit callback.');
82
+ }
83
+ else {
84
+ this.submitCallback('Authentication successful!');
85
+ await setTimeout(100);
86
+ }
87
+ log.info(`Your proxy rpc is ready! 🚀: \n\n${color.bold('http://127.0.0.1:24101/rpc')}\n`);
88
+ log.info('Waiting for logs...');
89
+ }
90
+ onMessage(msg) {
91
+ log.message(msg);
92
+ }
93
+ }
@@ -0,0 +1,80 @@
1
+ import Fastify from 'fastify';
2
+ import { validatorCompiler } from 'fastify-type-provider-zod';
3
+ import fastifyStatic from '@fastify/static';
4
+ import path from 'node:path';
5
+ import { z } from 'zod';
6
+ import { fastifyHttpProxy } from '@fastify/http-proxy';
7
+ import { randomBytes } from 'node:crypto';
8
+ function fastifyApp() {
9
+ return Fastify().withTypeProvider();
10
+ }
11
+ function randomStateString() {
12
+ return randomBytes(20).toString('hex');
13
+ }
14
+ export function buildServer(config) {
15
+ const app = fastifyApp();
16
+ app.setValidatorCompiler(validatorCompiler);
17
+ const state = randomStateString();
18
+ let jwt = '';
19
+ app.register(fastifyStatic, { root: path.join(import.meta.dirname, '..', 'static') });
20
+ app.get('/health', async (_req, reply) => {
21
+ return reply.send('ok');
22
+ });
23
+ app.get('/', async (_req, reply) => {
24
+ reply.header('Cache-Control', 'no-cache');
25
+ return reply.sendFile(path.join('start.html'));
26
+ });
27
+ app.get('/redirect-uri', async (_req, reply) => {
28
+ const url = new URL('/auth/authorize', config.userPanelUrl);
29
+ url.searchParams.set('client_id', 'proxy-cli');
30
+ url.searchParams.set('redirect_uri', `http://localhost:24101/callback`);
31
+ url.searchParams.set('state', state);
32
+ url.searchParams.set('response_type', 'token');
33
+ return reply.send(url.toString());
34
+ });
35
+ app.get('/callback', async (_req, reply) => {
36
+ reply.header('Cache-Control', 'no-cache');
37
+ return reply.sendFile(path.join('callback.html'));
38
+ });
39
+ app.post('/submit', {
40
+ schema: {
41
+ body: z.object({
42
+ token: z.string(),
43
+ state: z.string()
44
+ })
45
+ }
46
+ }, async (req, reply) => {
47
+ if (req.body.state !== state) {
48
+ throw new Error('invalid state received');
49
+ }
50
+ jwt = req.body.token;
51
+ await config.onSubmit();
52
+ return reply.send('ok');
53
+ });
54
+ app.register(fastifyHttpProxy, {
55
+ upstream: config.prividiumRpcUrl,
56
+ prefix: '/rpc',
57
+ rewritePrefix: '/rpc',
58
+ routes: ['/'],
59
+ preValidation: (request, _reply, done) => {
60
+ const parser = z.object({ method: z.string() });
61
+ const { method } = parser.parse(request.body);
62
+ config.onCall(method);
63
+ done();
64
+ },
65
+ preHandler: (_req, reply, done) => {
66
+ if (jwt === '') {
67
+ reply.status(500).send('please authenticate first.');
68
+ return;
69
+ }
70
+ return done();
71
+ },
72
+ replyOptions: {
73
+ rewriteRequestHeaders: (_originalReq, headers) => ({
74
+ ...headers,
75
+ authorization: `Bearer ${jwt}`
76
+ })
77
+ }
78
+ });
79
+ return app;
80
+ }
@@ -0,0 +1,70 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Prividium</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ </head>
9
+ <body
10
+ class="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 text-slate-800 flex items-center justify-center p-4"
11
+ >
12
+ <div
13
+ class="text-center bg-white/80 backdrop-blur-md py-10 px-6 shadow-xl rounded-2xl border border-slate-200 sm:px-12 w-full max-w-md"
14
+ >
15
+ <h1
16
+ id="main-title"
17
+ class="text-2xl md:text-3xl font-bold bg-gradient-to-r from-blue-700 to-blue-900 bg-clip-text text-transparent"
18
+ >
19
+ Finalizing Prividium sign-in...
20
+ </h1>
21
+ <div class="mt-4">
22
+ <svg
23
+ class="animate-spin h-8 w-8 text-blue-700 mx-auto"
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ fill="none"
26
+ viewBox="0 0 24 24"
27
+ >
28
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
29
+ <path
30
+ class="opacity-75"
31
+ fill="currentColor"
32
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
33
+ ></path>
34
+ </svg>
35
+ </div>
36
+ <div id="proxy-url" style="display: none" class="mt-6 text-blue-700 text-center">
37
+ <img src="https://www.zksync.io/faq/faq-brackets.svg" alt="Success" class="h-12 w-auto mx-auto mb-3" />
38
+ <span>You can now access your Prividium RPC proxy at: </span>
39
+ <span class="font-bold">http://127.0.0.1:24101/rpc</span>
40
+ </div>
41
+ </div>
42
+ <script>
43
+ const titleElem = document.getElementById('main-title');
44
+ const hashPath = window.location.hash;
45
+ const params = new URLSearchParams(hashPath.replace(/^#/, ''));
46
+ const maybeToken = params.get('token');
47
+ const maybeState = params.get('state');
48
+
49
+ if (!maybeToken || !maybeToken) {
50
+ console.error('Missing stuff');
51
+ } else {
52
+ fetch('/submit', {
53
+ method: 'POST',
54
+ headers: {
55
+ 'content-type': 'application/json'
56
+ },
57
+ body: JSON.stringify({
58
+ token: maybeToken,
59
+ state: maybeState
60
+ })
61
+ }).then(() => {
62
+ titleElem.innerHTML = 'Done! This window can be closed now.';
63
+ document.querySelector('svg').style.display = 'none';
64
+ const urlElem = document.getElementById('proxy-url');
65
+ urlElem.style.display = 'block';
66
+ }, console.error);
67
+ }
68
+ </script>
69
+ </body>
70
+ </html>
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Prividium</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ </head>
9
+ <body class="bg-gray-900 text-white flex items-center justify-center h-screen">
10
+ <div class="text-center">
11
+ <h1 id="main-title" class="text-2xl font-bold">Redirecting...</h1>
12
+ <div class="mt-4">
13
+ <svg class="animate-spin h-8 w-8 text-white mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
14
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
15
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
16
+ </svg>
17
+ </div>
18
+ </div>
19
+ <script>
20
+ const hashPath = window.location.hash;
21
+ const params = new URLSearchParams(hashPath.replace(/^#/, ''));
22
+
23
+ fetch('/redirect-uri').then(
24
+ (r) => {
25
+ return r.text();
26
+ },
27
+ console.error
28
+ ).then(url => {
29
+ window.location.assign(url);
30
+ });
31
+ </script>
32
+ </body>
33
+ </html>
@@ -1,6 +1,6 @@
1
1
  export { createPrividiumChain } from './prividium-chain.js';
2
- export type { PrividiumConfig, PrividiumChain, PopupOptions, Storage, TokenData, UserProfile, UserRole } from './types.js';
2
+ export type { PrividiumConfig, PrividiumChain, PopupOptions, Storage, TokenData, UserProfile, UserRole, AddNetworkParams } from './types.js';
3
3
  export { AUTH_ERRORS, STORAGE_KEYS } from './types.js';
4
4
  export { LocalStorage, TokenManager } from './storage.js';
5
5
  export { parseToken, isTokenExpired, generateRandomState } from './token-utils.js';
6
- export { PopupAuth, handleAuthCallback, type AuthCallbackMessage } from './popup-auth.js';
6
+ export { PopupAuth, handleAuthCallback, type AuthCallbackMessage, type OauthScope } from './popup-auth.js';
@@ -1,13 +1,18 @@
1
1
  import { type PopupOptions } from './types.js';
2
2
  import { type TokenManager } from './storage.js';
3
- export type OauthScope = 'wallet:required';
3
+ /**
4
+ * OAuth scopes that can be requested during authorization
5
+ *
6
+ * - `wallet:required`: Requires user to have at least one wallet associated
7
+ * - `network:required`: Requires user to have wallet connected with correct chain
8
+ */
9
+ export type OauthScope = 'wallet:required' | 'network:required';
4
10
  export interface PopupAuthConfig {
5
11
  authBaseUrl: string;
6
12
  clientId: string;
7
13
  redirectUri: string;
8
14
  tokenManager: TokenManager;
9
15
  onAuthExpiry?: () => void;
10
- scope?: OauthScope[];
11
16
  }
12
17
  export declare class PopupAuth {
13
18
  private config;
@@ -6,10 +6,10 @@ export class PopupAuth {
6
6
  this.config = config;
7
7
  }
8
8
  async authorize(options = {}) {
9
- const { popupSize = { w: 500, h: 600 } } = options;
9
+ const { popupSize = { w: 600, h: 800 }, scopes = [] } = options;
10
10
  const state = generateRandomState();
11
11
  this.config.tokenManager.setState(state);
12
- const authUrl = this.buildAuthUrl(state);
12
+ const authUrl = this.buildAuthUrl(state, scopes);
13
13
  const popup = this.openPopup(authUrl, popupSize);
14
14
  return new Promise((resolve, reject) => {
15
15
  const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
@@ -108,14 +108,14 @@ export class PopupAuth {
108
108
  }, TIMEOUT_MS);
109
109
  });
110
110
  }
111
- buildAuthUrl(state) {
111
+ buildAuthUrl(state, scopes) {
112
112
  const url = new URL('/auth/authorize', this.config.authBaseUrl);
113
113
  url.searchParams.set('client_id', this.config.clientId);
114
114
  url.searchParams.set('redirect_uri', this.config.redirectUri);
115
115
  url.searchParams.set('state', state);
116
116
  url.searchParams.set('response_type', 'token');
117
- if (this.config.scope?.length) {
118
- for (const scope of this.config.scope) {
117
+ if (scopes?.length) {
118
+ for (const scope of scopes) {
119
119
  url.searchParams.append('scope', scope);
120
120
  }
121
121
  }
@@ -0,0 +1,12 @@
1
+ import { type PrividiumConfig, type PrividiumChain } from './types.js';
2
+ declare global {
3
+ interface Window {
4
+ ethereum?: {
5
+ request: (args: {
6
+ method: string;
7
+ params?: unknown[];
8
+ }) => Promise<unknown>;
9
+ };
10
+ }
11
+ }
12
+ export declare function createPrividiumChain(config: PrividiumConfig): PrividiumChain;
@@ -0,0 +1,199 @@
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
+ tokenManager,
13
+ onAuthExpiry: config.onAuthExpiry
14
+ });
15
+ const getAuthHeaders = () => {
16
+ const token = tokenManager.getToken();
17
+ if (token) {
18
+ return {
19
+ Authorization: `Bearer ${token.rawToken}`
20
+ };
21
+ }
22
+ return null;
23
+ };
24
+ // Create transport with auth integration using viem callbacks
25
+ const transport = http(config.rpcUrl, {
26
+ batch: false,
27
+ fetchOptions: {
28
+ headers: getAuthHeaders() || {}
29
+ },
30
+ onFetchRequest(_request, init) {
31
+ init.headers = {
32
+ ...init.headers,
33
+ ...getAuthHeaders()
34
+ };
35
+ },
36
+ onFetchResponse: (response) => {
37
+ // Handle 403 responses (expired token or no access)
38
+ if (response.status === 403) {
39
+ tokenManager.clearToken();
40
+ config.onAuthExpiry?.();
41
+ }
42
+ }
43
+ });
44
+ return {
45
+ chain: {
46
+ ...chainConfig,
47
+ ...config.chain,
48
+ contracts: {
49
+ ...chainConfig.contracts,
50
+ ...config.chain.contracts,
51
+ multicall3: undefined // Prividium doesn't support multicall yet
52
+ },
53
+ rpcUrls: { default: { http: [config.rpcUrl] } }
54
+ },
55
+ transport,
56
+ async authorize(options) {
57
+ return popupAuth.authorize(options);
58
+ },
59
+ unauthorize() {
60
+ popupAuth.unauthorize();
61
+ },
62
+ isAuthorized() {
63
+ return popupAuth.isAuthorized();
64
+ },
65
+ getAuthHeaders,
66
+ async fetchUser() {
67
+ const headers = getAuthHeaders();
68
+ if (!headers) {
69
+ throw new Error('Authentication required. Please call authorize() first.');
70
+ }
71
+ const response = await fetch(`${config.permissionsApiBaseUrl}/api/profiles/me`, {
72
+ method: 'GET',
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ ...headers
76
+ }
77
+ });
78
+ if (response.status === 403) {
79
+ tokenManager.clearToken();
80
+ config.onAuthExpiry?.();
81
+ throw new Error('Authentication required. Please call authorize() first.');
82
+ }
83
+ if (!response.ok) {
84
+ throw new Error(`Failed to fetch user profile: ${response.status} ${response.statusText}`);
85
+ }
86
+ const userData = (await response.json());
87
+ return {
88
+ userId: userData.userId,
89
+ createdAt: new Date(userData.createdAt),
90
+ displayName: userData.displayName,
91
+ updatedAt: new Date(userData.updatedAt),
92
+ roles: userData.roles,
93
+ walletAddresses: userData.walletAddresses
94
+ };
95
+ },
96
+ async getWalletToken() {
97
+ const headers = getAuthHeaders();
98
+ if (!headers) {
99
+ throw new Error('Authentication required. Please call authorize() first.');
100
+ }
101
+ const response = await fetch(`${config.permissionsApiBaseUrl}/api/wallet/personal-rpc-token`, {
102
+ method: 'GET',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ ...headers
106
+ }
107
+ });
108
+ if (response.status === 403) {
109
+ tokenManager.clearToken();
110
+ config.onAuthExpiry?.();
111
+ throw new Error('Authentication required. Please call authorize() first.');
112
+ }
113
+ if (!response.ok) {
114
+ throw new Error(`Failed to fetch wallet token: ${response.status} ${response.statusText}`);
115
+ }
116
+ const data = (await response.json());
117
+ return data.token;
118
+ },
119
+ async getWalletRpcUrl() {
120
+ const walletToken = await this.getWalletToken();
121
+ // Extract base URL without /api path
122
+ const baseUrl = config.rpcUrl.replace(/\/rpc.*$/, '');
123
+ return `${baseUrl}/wallet/${walletToken}`;
124
+ },
125
+ async invalidateWalletToken() {
126
+ const headers = getAuthHeaders();
127
+ if (!headers) {
128
+ throw new Error('Authentication required. Please call authorize() first.');
129
+ }
130
+ const response = await fetch(`${config.permissionsApiBaseUrl}/api/wallet/invalidate`, {
131
+ method: 'POST',
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ ...headers
135
+ }
136
+ });
137
+ if (!response.ok) {
138
+ throw new Error(`Failed to invalidate wallet token: ${response.status} ${response.statusText}`);
139
+ }
140
+ const result = (await response.json());
141
+ return result.newWalletToken;
142
+ },
143
+ async enableWalletToken(params) {
144
+ const headers = getAuthHeaders();
145
+ if (!headers) {
146
+ throw new Error('Authentication required. Please call authorize() first.');
147
+ }
148
+ const response = await fetch(`${config.permissionsApiBaseUrl}/api/wallet/enable`, {
149
+ method: 'POST',
150
+ headers: {
151
+ 'Content-Type': 'application/json',
152
+ ...headers
153
+ },
154
+ body: JSON.stringify(params)
155
+ });
156
+ if (response.status === 403) {
157
+ tokenManager.clearToken();
158
+ config.onAuthExpiry?.();
159
+ throw new Error('Authentication required. Please call authorize() first.');
160
+ }
161
+ if (!response.ok) {
162
+ throw new Error(`Failed to enable wallet token: ${response.status} ${response.statusText}`);
163
+ }
164
+ const result = (await response.json());
165
+ return result;
166
+ },
167
+ async addNetworkToWallet(params) {
168
+ if (typeof window === 'undefined' || !window.ethereum) {
169
+ throw new Error('Wallet not detected. Please install a wallet extension to add network.');
170
+ }
171
+ const walletRpcUrl = await this.getWalletRpcUrl();
172
+ const chainIdHex = `0x${config.chain.id.toString(16)}`;
173
+ const networkParams = {
174
+ chainId: params?.chainId || chainIdHex,
175
+ chainName: params?.chainName || config.chain.name,
176
+ nativeCurrency: params?.nativeCurrency || {
177
+ name: config.chain.nativeCurrency?.name || 'Ether',
178
+ symbol: config.chain.nativeCurrency?.symbol || 'ETH',
179
+ decimals: config.chain.nativeCurrency?.decimals || 18
180
+ },
181
+ rpcUrls: [walletRpcUrl],
182
+ blockExplorerUrls: params?.blockExplorerUrls ||
183
+ (config.chain.blockExplorers?.default?.url ? [config.chain.blockExplorers.default.url] : undefined)
184
+ };
185
+ try {
186
+ if (!window.ethereum) {
187
+ throw new Error('Wallet not detected');
188
+ }
189
+ await window.ethereum.request({
190
+ method: 'wallet_addEthereumChain',
191
+ params: [networkParams]
192
+ });
193
+ }
194
+ catch (error) {
195
+ throw new Error(`Failed to add network to wallet: ${error instanceof Error ? error.message : 'Unknown error'}`);
196
+ }
197
+ }
198
+ };
199
+ }
@@ -1,4 +1,4 @@
1
- import { type Chain, type Transport } from 'viem';
1
+ import { type Chain, type Transport, type Address, type Hex } from 'viem';
2
2
  import { type OauthScope } from './popup-auth.js';
3
3
  export interface Storage {
4
4
  getItem(key: string): string | null;
@@ -12,7 +12,6 @@ export interface PrividiumConfig {
12
12
  authBaseUrl: string;
13
13
  redirectUrl: string;
14
14
  permissionsApiBaseUrl: string;
15
- scope?: OauthScope[];
16
15
  storage?: Storage;
17
16
  onAuthExpiry?: () => void;
18
17
  }
@@ -27,19 +26,39 @@ export interface UserProfile {
27
26
  roles: UserRole[];
28
27
  walletAddresses: string[];
29
28
  }
29
+ export interface AddNetworkParams {
30
+ chainName?: string;
31
+ chainId: string;
32
+ nativeCurrency?: {
33
+ name: string;
34
+ symbol: string;
35
+ decimals: number;
36
+ };
37
+ blockExplorerUrls?: string[];
38
+ }
39
+ export interface EnableWalletTokenParams {
40
+ walletAddress: Address;
41
+ contractAddress: Address;
42
+ nonce: number;
43
+ calldata: Hex;
44
+ }
45
+ export interface EnableWalletTokenResponse {
46
+ message: string;
47
+ activeUntil: string;
48
+ }
30
49
  export interface PrividiumChain {
31
50
  chain: Chain;
32
51
  transport: Transport;
33
- authorize(opts?: {
34
- popupSize?: {
35
- w: number;
36
- h: number;
37
- };
38
- }): Promise<string>;
52
+ authorize(opts?: PopupOptions): Promise<string>;
39
53
  unauthorize(): void;
40
54
  isAuthorized(): boolean;
41
55
  getAuthHeaders(): Record<string, string> | null;
42
56
  fetchUser(): Promise<UserProfile>;
57
+ getWalletToken(): Promise<string>;
58
+ getWalletRpcUrl(): Promise<string>;
59
+ invalidateWalletToken(): Promise<string>;
60
+ enableWalletToken(params: EnableWalletTokenParams): Promise<EnableWalletTokenResponse>;
61
+ addNetworkToWallet(params?: AddNetworkParams): Promise<void>;
43
62
  }
44
63
  export interface TokenData {
45
64
  rawToken: string;
@@ -52,6 +71,7 @@ export interface PopupOptions {
52
71
  w: number;
53
72
  h: number;
54
73
  };
74
+ scopes?: OauthScope[];
55
75
  }
56
76
  export declare const AUTH_ERRORS: {
57
77
  readonly INVALID_STATE: "Invalid state parameter";
@@ -0,0 +1 @@
1
+ {"root":["../cli/base-cli.ts","../cli/index.ts","../cli/commands/config.ts","../cli/commands/proxy.ts","../cli/server/config-file.ts","../cli/server/connection-workflow.ts","../cli/server/server.ts"],"version":"5.8.3"}
@@ -0,0 +1 @@
1
+ {"root":["../src/index.ts","../src/popup-auth.ts","../src/prividium-chain.ts","../src/storage.ts","../src/token-utils.ts","../src/types.ts"],"version":"5.8.3"}
package/package.json CHANGED
@@ -1,26 +1,40 @@
1
1
  {
2
2
  "name": "prividium",
3
- "version": "0.0.1-beta",
3
+ "version": "0.0.3-beta",
4
+ "bin": {
5
+ "prividium": "./bin/cli.js"
6
+ },
4
7
  "exports": {
5
8
  ".": {
6
- "import": "./dist/index.js",
7
- "types": "./dist/index.d.ts"
9
+ "import": "./dist/sdk/index.js",
10
+ "types": "./dist/sdk/index.d.ts"
8
11
  }
9
12
  },
10
13
  "files": [
11
14
  "dist"
12
15
  ],
13
16
  "scripts": {
14
- "build": "tsc",
17
+ "build": "tsc -b && yarn copy-static-files",
18
+ "copy-static-files": "cp -r cli/static dist/cli",
19
+ "dev": "tsc -b --watch",
15
20
  "lint": "eslint . --ignore-path ../../.gitignore --max-warnings 0",
16
21
  "lint:fix": "eslint . --fix --ignore-path ../../.gitignore",
17
22
  "test": "vitest run",
18
23
  "test:watch": "vitest",
19
- "typecheck": "tsc --noEmit",
24
+ "typecheck": "tsc -b --noEmit",
25
+ "cli": "tsx cli/index.ts",
20
26
  "typecheck:test": "tsc --project tsconfig.test.json --noEmit"
21
27
  },
22
28
  "dependencies": {
23
- "zod": "^3.23.8"
29
+ "@clack/prompts": "^0.11.0",
30
+ "@fastify/http-proxy": "^11.3.0",
31
+ "@fastify/static": "^8.3.0",
32
+ "appdirsjs": "^1.2.7",
33
+ "fastify": "^5.0.0",
34
+ "fastify-type-provider-zod": "^6.1.0",
35
+ "kleur": "^4.1.5",
36
+ "yargs": "^18.0.0",
37
+ "zod": "^4.1.12"
24
38
  },
25
39
  "peerDependencies": {
26
40
  "viem": ">=2.0.0"
@@ -28,8 +42,10 @@
28
42
  "devDependencies": {
29
43
  "@repo/eslint-config": "workspace:*",
30
44
  "@types/node": "^22.8.6",
45
+ "@types/yargs": "^17.0.34",
31
46
  "eslint": "^8",
32
47
  "jsdom": "^25.0.0",
48
+ "tsx": "^4.20.6",
33
49
  "typescript": "^5.8.3",
34
50
  "vitest": "^3.2.4"
35
51
  }
@@ -1,2 +0,0 @@
1
- import { type PrividiumConfig, type PrividiumChain } from './types.js';
2
- export declare function createPrividiumChain(config: PrividiumConfig): PrividiumChain;
@@ -1,98 +0,0 @@
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
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes