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 +87 -3
- package/bin/cli.js +2 -0
- package/dist/cli/base-cli.js +9 -0
- package/dist/cli/commands/config.js +40 -0
- package/dist/cli/commands/proxy.js +71 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/server/config-file.js +58 -0
- package/dist/cli/server/connection-workflow.js +93 -0
- package/dist/cli/server/server.js +80 -0
- package/dist/cli/static/callback.html +70 -0
- package/dist/cli/static/start.html +33 -0
- package/dist/{index.d.ts → sdk/index.d.ts} +2 -2
- package/dist/{popup-auth.d.ts → sdk/popup-auth.d.ts} +7 -2
- package/dist/{popup-auth.js → sdk/popup-auth.js} +5 -5
- package/dist/sdk/prividium-chain.d.ts +12 -0
- package/dist/sdk/prividium-chain.js +199 -0
- package/dist/{types.d.ts → sdk/types.d.ts} +28 -8
- package/dist/tsconfig.cli.tsbuildinfo +1 -0
- package/dist/tsconfig.sdk.tsbuildinfo +1 -0
- package/package.json +22 -6
- package/dist/prividium-chain.d.ts +0 -2
- package/dist/prividium-chain.js +0 -98
- /package/dist/{index.js → sdk/index.js} +0 -0
- /package/dist/{storage.d.ts → sdk/storage.d.ts} +0 -0
- /package/dist/{storage.js → sdk/storage.js} +0 -0
- /package/dist/{token-utils.d.ts → sdk/token-utils.d.ts} +0 -0
- /package/dist/{token-utils.js → sdk/token-utils.js} +0 -0
- /package/dist/{types.js → sdk/types.js} +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Prividium SDK
|
|
2
2
|
|
|
3
|
-
A TypeScript SDK for integrating with Prividium
|
|
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.
|
|
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,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
|
-
|
|
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:
|
|
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 (
|
|
118
|
-
for (const scope of
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|
package/dist/prividium-chain.js
DELETED
|
@@ -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
|