prividium 0.17.0 → 0.18.0
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 +31 -18
- package/bin/cli.js +1 -1
- package/dist/cli/{commands → cli/commands}/config.js +4 -12
- package/dist/cli/cli/commands/proxy.js +255 -0
- package/dist/cli/{commands → cli/commands}/utils/url-config.js +20 -3
- package/dist/cli/{server → cli/server}/config-file.js +28 -8
- package/dist/cli/{server → cli/server}/connection-workflow.js +1 -1
- package/dist/cli/{server → cli/server}/server.js +32 -13
- package/dist/cli/{static → cli/static}/callback.html +1 -1
- package/dist/cli/src/error-utils.js +73 -0
- package/dist/cli/src/memory-storage.js +12 -0
- package/dist/cli/src/rpc-error-codes.js +24 -0
- package/dist/cli/src/siwe-auth.js +70 -0
- package/dist/cli/src/storage.js +142 -0
- package/dist/cli/src/types.js +46 -0
- package/dist/cli/tsconfig.cli.tsbuildinfo +1 -0
- package/dist/sdk/error-utils.d.ts +1 -0
- package/dist/sdk/error-utils.js +20 -6
- package/dist/sdk/popup-auth.d.ts +1 -7
- package/dist/sdk/siwe-auth.d.ts +1 -1
- package/dist/sdk/siwe-auth.js +1 -1
- package/dist/sdk/siwe-chain.d.ts +1 -1
- package/dist/sdk/siwe-chain.js +23 -19
- package/dist/sdk/types.d.ts +1 -1
- package/package.json +2 -2
- package/dist/cli/commands/proxy.js +0 -149
- package/dist/tsconfig.cli.tsbuildinfo +0 -1
- /package/dist/cli/{base-cli.js → cli/base-cli.js} +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/clients/browser-auth.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/clients/http.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/clients/rpc.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/clients/wallet-api.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/constants.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/authentication/authentication.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/authentication/wallet-preconditions.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/authentication.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/bridging/bridging.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/bridging.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/global/input-validation.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/global/reachability.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/global.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/wallet/authenticated-rpc.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/wallet/authorization-and-wallet-rpc.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/wallet-api.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/probes/wallet.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/profile.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/report/build.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/report/render.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/stages.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/types.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor/utils.js +0 -0
- /package/dist/cli/{commands → cli/commands}/doctor.js +0 -0
- /package/dist/cli/{commands → cli/commands}/utils/show-prividium-header.js +0 -0
- /package/dist/cli/{index.js → cli/index.js} +0 -0
- /package/dist/cli/{static → cli/static}/start.html +0 -0
package/README.md
CHANGED
|
@@ -404,7 +404,7 @@ interface PrividiumSiweConfig {
|
|
|
404
404
|
chain: Chain; // Viem chain configuration (without rpcUrls)
|
|
405
405
|
prividiumApiBaseUrl: string; // Permissions API service base URL
|
|
406
406
|
account: LocalAccount; // Viem account for signing (e.g. from privateKeyToAccount)
|
|
407
|
-
domain
|
|
407
|
+
domain?: string; // Optional: domain for SIWE message (defaults to server-configured domain)
|
|
408
408
|
storage?: Storage; // Custom storage (defaults to MemoryStorage)
|
|
409
409
|
autoReauthenticate?: boolean; // Auto-reauth on session expiry (default: true)
|
|
410
410
|
onAuthExpiry?: () => void; // Called when auth expires and reauth is disabled/failed
|
|
@@ -542,27 +542,43 @@ to use your existing deployment workflows.
|
|
|
542
542
|
|
|
543
543
|
### Core Command
|
|
544
544
|
|
|
545
|
+
Browser login mode (interactive):
|
|
546
|
+
|
|
545
547
|
```bash
|
|
546
548
|
prividium proxy \
|
|
547
|
-
--rpc-url https://<your-prividium-rpc> \
|
|
548
549
|
--user-panel-url https://<your-user-panel> \
|
|
549
550
|
[--port 24101] [--host 127.0.0.1] [--config-path <path>]
|
|
550
551
|
```
|
|
551
552
|
|
|
552
|
-
|
|
553
|
+
Private key mode (non-interactive):
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
prividium proxy \
|
|
557
|
+
--api-url https://<your-prividium-api> \
|
|
558
|
+
--private-key 0x<your-private-key>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
What happens (browser login mode):
|
|
553
562
|
|
|
554
|
-
-
|
|
555
|
-
Panel URL is detected; warns and continues if the URL cannot be reached or confirmed.
|
|
563
|
+
- Discovers the API URL automatically from the User Panel's `prividium-api-url` meta tag.
|
|
556
564
|
- Prints a local URL to open in your browser for sign-in.
|
|
557
|
-
- After successful sign-in, the proxy is available at `http://127.0.0.1:24101
|
|
558
|
-
- All requests are forwarded to
|
|
565
|
+
- After successful sign-in, the proxy is available at `http://127.0.0.1:24101`.
|
|
566
|
+
- All requests are forwarded to the API with `Authorization: Bearer <token>`.
|
|
559
567
|
- Requests are rejected until authentication completes.
|
|
560
568
|
|
|
569
|
+
What happens (private key mode):
|
|
570
|
+
|
|
571
|
+
- Authenticates immediately using SIWE with the provided private key (no browser required).
|
|
572
|
+
- Re-authenticates automatically when the session expires.
|
|
573
|
+
- Proxy is available at `http://127.0.0.1:24101` once authenticated.
|
|
574
|
+
|
|
561
575
|
Flags:
|
|
562
576
|
|
|
563
|
-
- `--rpc-url, -r` (string): Target Prividium™
|
|
564
|
-
- `--user-panel-url, -u` (string): URL of the Prividium™ User Panel
|
|
565
|
-
|
|
577
|
+
- `--api-url, --rpc-url, -r` (string): Target Prividium™ API URL. Required with `--private-key`.
|
|
578
|
+
- `--user-panel-url, -u` (string): URL of the Prividium™ User Panel for browser-based login. The API URL is discovered
|
|
579
|
+
automatically from the User Panel when `--api-url` is not provided.
|
|
580
|
+
- `--private-key` (string): Ethereum private key for SIWE authentication (skips browser login). Can also be set via
|
|
581
|
+
`PRIVIDIUM_PRIVATE_KEY`.
|
|
566
582
|
- `--port, -p` (number, default `24101`): Local proxy port. This has to match with the port configured in the admin
|
|
567
583
|
panel, which by default uses this same port.
|
|
568
584
|
- `--host, -h` (string, default `127.0.0.1`): host binded to server. By default only connections comming from local
|
|
@@ -572,7 +588,8 @@ Flags:
|
|
|
572
588
|
|
|
573
589
|
Environment variables (optional):
|
|
574
590
|
|
|
575
|
-
- `PRIVIDIUM_RPC_URL`
|
|
591
|
+
- `PRIVIDIUM_API_URL` (or `PRIVIDIUM_RPC_URL`)
|
|
592
|
+
- `PRIVIDIUM_PRIVATE_KEY`
|
|
576
593
|
- `USER_PANEL_URL`
|
|
577
594
|
|
|
578
595
|
Precedence: CLI flags > environment variables > saved config file.
|
|
@@ -582,19 +599,15 @@ Precedence: CLI flags > environment variables > saved config file.
|
|
|
582
599
|
When running the full Prividium stack locally (`pnpm dev`), use:
|
|
583
600
|
|
|
584
601
|
```bash
|
|
585
|
-
npx prividium proxy
|
|
586
|
-
--rpc-url http://localhost:8000 \
|
|
587
|
-
--user-panel-url http://localhost:3001
|
|
602
|
+
npx prividium proxy --user-panel-url http://localhost:3001
|
|
588
603
|
```
|
|
589
604
|
|
|
590
|
-
Point Foundry, Hardhat, or your scripts at `http://127.0.0.1:24101
|
|
605
|
+
Point Foundry, Hardhat, or your scripts at `http://127.0.0.1:24101` after sign-in.
|
|
591
606
|
|
|
592
607
|
### Config Commands
|
|
593
608
|
|
|
594
609
|
```bash
|
|
595
|
-
prividium config set
|
|
596
|
-
--rpc-url https://<your-prividium-rpc> \
|
|
597
|
-
--user-panel-url https://<your-user-panel>
|
|
610
|
+
prividium config set --api-url https://<your-prividium-api>
|
|
598
611
|
|
|
599
612
|
prividium config print # Show current values
|
|
600
613
|
prividium config path # Show config file location
|
package/bin/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import '../dist/cli/index.js';
|
|
2
|
+
import '../dist/cli/cli/index.js';
|
|
@@ -20,21 +20,13 @@ export const addConfig = (cli) => {
|
|
|
20
20
|
.command('clear', 'Clears current configuration file', (yargs) => yargs, (args) => clearConfig(args.configPath))
|
|
21
21
|
.command('path', 'Shows local config file path', (yargs) => yargs, (args) => configPath(args.configPath))
|
|
22
22
|
.command('print', 'Prints current configuration', (yargs) => yargs, (args) => printConfig(args.configPath))
|
|
23
|
-
.command('set', 'Updates config', (yargs) => yargs
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
description: 'Specifies target Prividium™ rpc url. These takes precedence over config file and env variable.',
|
|
23
|
+
.command('set', 'Updates config', (yargs) => yargs.option('apiUrl', {
|
|
24
|
+
alias: ['api-url', 'rpc-url', 'r'],
|
|
25
|
+
description: 'Specifies target Prividium™ API url.',
|
|
27
26
|
demandOption: true,
|
|
28
27
|
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
28
|
}), (args) => updateConfig({
|
|
36
|
-
|
|
37
|
-
userPanelUrl: args.userPanelUrl
|
|
29
|
+
apiUrl: args.apiUrl
|
|
38
30
|
}, args.configPath))
|
|
39
31
|
.demandCommand());
|
|
40
32
|
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { intro, log, text } from '@clack/prompts';
|
|
2
|
+
import color from 'kleur';
|
|
3
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { MemoryStorage } from '../../src/memory-storage.js';
|
|
6
|
+
import { SiweAuth } from '../../src/siwe-auth.js';
|
|
7
|
+
import { TokenManager } from '../../src/storage.js';
|
|
8
|
+
import { CreationWorkflow } from '../server/connection-workflow.js';
|
|
9
|
+
import { buildServer } from '../server/server.js';
|
|
10
|
+
import { showPrividiumHeader } from './utils/show-prividium-header.js';
|
|
11
|
+
import { gatherApiUrl } from './utils/url-config.js';
|
|
12
|
+
const DEFAULT_PORT = 24101;
|
|
13
|
+
const PRIVATE_KEY_REGEX = /^0x[0-9a-fA-F]{64}$/;
|
|
14
|
+
function checkHostAndPortWarnings(host, port, allowExternalAccess) {
|
|
15
|
+
if (port !== DEFAULT_PORT) {
|
|
16
|
+
log.warn(`Non standard port detected: ${port}. Redirect from prividium auth might not work.`);
|
|
17
|
+
}
|
|
18
|
+
if (host !== '127.0.0.1' && host !== 'localhost') {
|
|
19
|
+
if (!allowExternalAccess) {
|
|
20
|
+
log.error(`${color.bold('ERROR')}: In order to use a host different than local host you need to set --unsecureAllowOutsideAccess flag.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (host === '0.0.0.0') {
|
|
24
|
+
log.warn(`${color.bold('WARNING')}: Your local proxy will be exposed outside your current device.`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
log.warn(`${color.bold('WARNING')}: Non standard host: ${host}. Your proxy might be open for other devices. Redirect from prividium auth might not work.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const wellKnownMetadataSchema = z.object({
|
|
32
|
+
apiUrl: z.string().optional()
|
|
33
|
+
});
|
|
34
|
+
// Fetches /.well-known/prividium from the User Panel and returns its apiUrl.
|
|
35
|
+
async function discoverApiUrlFromUserPanel(userPanelUrl) {
|
|
36
|
+
const wellKnownUrl = new URL('/.well-known/prividium', userPanelUrl).toString();
|
|
37
|
+
let response;
|
|
38
|
+
try {
|
|
39
|
+
response = await fetch(wellKnownUrl, { signal: AbortSignal.timeout(5000) });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new Error(`Could not reach User Panel at ${userPanelUrl}.\n` +
|
|
43
|
+
` ${error instanceof Error ? error.message : String(error)}\n` +
|
|
44
|
+
' Please verify the URL is correct and the User Panel is running.');
|
|
45
|
+
}
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`User Panel at ${userPanelUrl} did not return /.well-known/prividium (HTTP ${response.status}).\n` +
|
|
48
|
+
' Please verify the URL points to a Prividium™ User Panel, or pass --api-url directly.');
|
|
49
|
+
}
|
|
50
|
+
let json;
|
|
51
|
+
try {
|
|
52
|
+
json = await response.json();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error(`User Panel at ${userPanelUrl} returned non-JSON for /.well-known/prividium.\n` +
|
|
56
|
+
' Please verify the URL points to a Prividium™ User Panel, or pass --api-url directly.');
|
|
57
|
+
}
|
|
58
|
+
const parsed = wellKnownMetadataSchema.safeParse(json);
|
|
59
|
+
if (!parsed.success || !parsed.data.apiUrl) {
|
|
60
|
+
throw new Error(`User Panel at ${userPanelUrl} did not advertise an apiUrl in /.well-known/prividium.\n` +
|
|
61
|
+
' Please pass --api-url directly.');
|
|
62
|
+
}
|
|
63
|
+
return parsed.data.apiUrl;
|
|
64
|
+
}
|
|
65
|
+
async function resolveUserPanelUrl(opts) {
|
|
66
|
+
const explicit = opts.userPanelUrl ?? process.env.USER_PANEL_URL;
|
|
67
|
+
if (explicit) {
|
|
68
|
+
const res = z.url().safeParse(explicit);
|
|
69
|
+
if (!res.success) {
|
|
70
|
+
throw new Error(`Invalid user panel url provided: ${explicit}`);
|
|
71
|
+
}
|
|
72
|
+
log.info(`Using user panel url: ${explicit}`);
|
|
73
|
+
return explicit;
|
|
74
|
+
}
|
|
75
|
+
const prompted = await text({
|
|
76
|
+
message: 'Please insert your Prividium™ User Panel url',
|
|
77
|
+
validate(value) {
|
|
78
|
+
const parsed = z.url().safeParse(value);
|
|
79
|
+
if (!parsed.success) {
|
|
80
|
+
return 'Please provide a valid url';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
if (typeof prompted === 'symbol') {
|
|
85
|
+
throw new Error('Canceled by the user');
|
|
86
|
+
}
|
|
87
|
+
return prompted;
|
|
88
|
+
}
|
|
89
|
+
async function startServerWithPrivateKey(opts) {
|
|
90
|
+
const privateKey = opts.privateKey;
|
|
91
|
+
if (!PRIVATE_KEY_REGEX.test(privateKey)) {
|
|
92
|
+
log.error('Invalid private key format. Expected 0x-prefixed 32-byte hex string.');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const account = privateKeyToAccount(privateKey);
|
|
96
|
+
showPrividiumHeader();
|
|
97
|
+
intro('Starting Prividium™ proxy (private key auth)');
|
|
98
|
+
const apiUrl = await gatherApiUrl({
|
|
99
|
+
configPath: opts.configPath,
|
|
100
|
+
apiUrl: opts.apiUrl,
|
|
101
|
+
logProvidedUrls: true
|
|
102
|
+
});
|
|
103
|
+
checkHostAndPortWarnings(opts.host, opts.port, opts.allowExternalAccess);
|
|
104
|
+
const storage = new MemoryStorage();
|
|
105
|
+
const tokenManager = new TokenManager(storage, 0, apiUrl);
|
|
106
|
+
const siweAuth = new SiweAuth({
|
|
107
|
+
account,
|
|
108
|
+
prividiumApiBaseUrl: apiUrl,
|
|
109
|
+
tokenManager
|
|
110
|
+
});
|
|
111
|
+
log.info(`Authenticating as ${account.address}...`);
|
|
112
|
+
const tokenData = await siweAuth.authorize();
|
|
113
|
+
log.info('Authentication successful');
|
|
114
|
+
const app = buildServer({
|
|
115
|
+
apiUrl,
|
|
116
|
+
host: opts.host,
|
|
117
|
+
port: opts.port,
|
|
118
|
+
initialToken: {
|
|
119
|
+
accessToken: tokenData.rawToken,
|
|
120
|
+
expiresAt: tokenData.expiresAt
|
|
121
|
+
},
|
|
122
|
+
async onReAuth() {
|
|
123
|
+
log.info('Session expired, re-authenticating...');
|
|
124
|
+
const newToken = await siweAuth.authorize();
|
|
125
|
+
log.info('Re-authentication successful');
|
|
126
|
+
return { accessToken: newToken.rawToken, expiresAt: newToken.expiresAt };
|
|
127
|
+
},
|
|
128
|
+
async onSubmit() { },
|
|
129
|
+
onCall(methodName) {
|
|
130
|
+
log.message(methodName);
|
|
131
|
+
},
|
|
132
|
+
onReAuthNeeded() { },
|
|
133
|
+
onError(err) {
|
|
134
|
+
log.error(`Error: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
await app.listen({
|
|
138
|
+
port: opts.port,
|
|
139
|
+
host: opts.host
|
|
140
|
+
});
|
|
141
|
+
const serverUrl = `http://${opts.host}:${opts.port}`;
|
|
142
|
+
log.info(`Your proxy rpc is ready: \n\n${color.bold(serverUrl)}\n`);
|
|
143
|
+
log.info('Waiting for logs...');
|
|
144
|
+
}
|
|
145
|
+
async function startServer(opts) {
|
|
146
|
+
const workflow = new CreationWorkflow(opts.host, opts.port);
|
|
147
|
+
workflow.start();
|
|
148
|
+
const userPanelUrl = await resolveUserPanelUrl(opts);
|
|
149
|
+
let apiUrl;
|
|
150
|
+
if (opts.apiUrl) {
|
|
151
|
+
apiUrl = opts.apiUrl;
|
|
152
|
+
log.info(`Using Prividium™ API url: ${apiUrl}`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
log.info('Discovering API URL from User Panel...');
|
|
156
|
+
apiUrl = await discoverApiUrlFromUserPanel(userPanelUrl);
|
|
157
|
+
log.info(`Using Prividium™ API url: ${apiUrl}`);
|
|
158
|
+
}
|
|
159
|
+
checkHostAndPortWarnings(opts.host, opts.port, opts.allowExternalAccess);
|
|
160
|
+
const serverUrl = `http://${opts.host}:${opts.port}`;
|
|
161
|
+
const app = buildServer({
|
|
162
|
+
apiUrl,
|
|
163
|
+
userPanelUrl,
|
|
164
|
+
host: opts.host,
|
|
165
|
+
port: opts.port,
|
|
166
|
+
async onSubmit() {
|
|
167
|
+
await workflow.onSubmit();
|
|
168
|
+
},
|
|
169
|
+
onCall(methodName) {
|
|
170
|
+
workflow.onMessage(methodName);
|
|
171
|
+
},
|
|
172
|
+
onReAuthNeeded() {
|
|
173
|
+
workflow.onMessage(`Please login again: ${serverUrl}`);
|
|
174
|
+
},
|
|
175
|
+
onError: (err) => {
|
|
176
|
+
workflow.onError(err);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
await app.listen({
|
|
180
|
+
port: opts.port,
|
|
181
|
+
host: opts.host
|
|
182
|
+
});
|
|
183
|
+
await workflow.waitForAuthentication(serverUrl);
|
|
184
|
+
}
|
|
185
|
+
export const addProxy = (cli) => {
|
|
186
|
+
return cli.command('proxy', 'Starts authenticated rpc proxy server', (yargs) => yargs
|
|
187
|
+
.option('apiUrl', {
|
|
188
|
+
alias: ['api-url', 'rpc-url', 'r'],
|
|
189
|
+
description: 'Specifies target Prividium™ API url. Required with --private-key.',
|
|
190
|
+
demandOption: false,
|
|
191
|
+
type: 'string'
|
|
192
|
+
})
|
|
193
|
+
.option('userPanelUrl', {
|
|
194
|
+
alias: ['user-panel-url', 'u'],
|
|
195
|
+
description: 'Specifies the User Panel url for browser-based login. If --api-url is omitted, it is fetched from the User Panel via /.well-known/prividium.',
|
|
196
|
+
type: 'string'
|
|
197
|
+
})
|
|
198
|
+
.option('configPath', {
|
|
199
|
+
alias: ['c', 'config-path', 'config'],
|
|
200
|
+
description: 'Path for config file. By default config file is stored under user personal folder',
|
|
201
|
+
type: 'string',
|
|
202
|
+
demandOption: false
|
|
203
|
+
})
|
|
204
|
+
.option('port', {
|
|
205
|
+
alias: ['p'],
|
|
206
|
+
description: 'Port used for local proxy. This has to match with the port configured in your Prividium™ network.',
|
|
207
|
+
default: DEFAULT_PORT,
|
|
208
|
+
type: 'number'
|
|
209
|
+
})
|
|
210
|
+
.option('host', {
|
|
211
|
+
alias: 'h',
|
|
212
|
+
description: 'Host used for local server. By default traffic from outside localhost is disabled.',
|
|
213
|
+
default: '127.0.0.1',
|
|
214
|
+
type: 'string'
|
|
215
|
+
})
|
|
216
|
+
.option('unsecureAllowOutsideAccess', {
|
|
217
|
+
alias: ['unsecure-allow-outside-access'],
|
|
218
|
+
description: 'Allow server to be exposed to the network (accessible by other devices)',
|
|
219
|
+
default: false,
|
|
220
|
+
type: 'boolean'
|
|
221
|
+
})
|
|
222
|
+
.option('privateKey', {
|
|
223
|
+
alias: ['private-key'],
|
|
224
|
+
description: 'Ethereum private key for SIWE authentication (skips browser login). Can also be set via PRIVIDIUM_PRIVATE_KEY env var.',
|
|
225
|
+
type: 'string'
|
|
226
|
+
}), async (args) => {
|
|
227
|
+
try {
|
|
228
|
+
const privateKey = args.privateKey ?? process.env.PRIVIDIUM_PRIVATE_KEY;
|
|
229
|
+
const rpcUrlEnv = process.env.PRIVIDIUM_RPC_URL;
|
|
230
|
+
const apiUrl = args.apiUrl ??
|
|
231
|
+
process.env.PRIVIDIUM_API_URL ??
|
|
232
|
+
(rpcUrlEnv ? rpcUrlEnv.replace(/\/rpc\/?$/, '') : undefined);
|
|
233
|
+
const opts = {
|
|
234
|
+
apiUrl,
|
|
235
|
+
userPanelUrl: args.userPanelUrl,
|
|
236
|
+
privateKey,
|
|
237
|
+
configPath: args.configPath,
|
|
238
|
+
port: args.port,
|
|
239
|
+
host: args.host,
|
|
240
|
+
allowExternalAccess: args.unsecureAllowOutsideAccess
|
|
241
|
+
};
|
|
242
|
+
if (privateKey) {
|
|
243
|
+
await startServerWithPrivateKey(opts);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
await startServer(opts);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
console.error(`Prividium proxy failed: ${message}`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
};
|
|
@@ -2,6 +2,7 @@ import { confirm, log, text } from '@clack/prompts';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { ConfigFile } from '../../server/config-file.js';
|
|
4
4
|
const envSchema = z.object({
|
|
5
|
+
PRIVIDIUM_API_URL: z.string().optional(),
|
|
5
6
|
PRIVIDIUM_RPC_URL: z.string().optional(),
|
|
6
7
|
USER_PANEL_URL: z.string().optional()
|
|
7
8
|
});
|
|
@@ -30,12 +31,28 @@ async function askForUrl(maybeUrl, name, logProvidedUrls) {
|
|
|
30
31
|
}
|
|
31
32
|
return { url, prompted: true };
|
|
32
33
|
}
|
|
34
|
+
export async function gatherApiUrl({ configPath, apiUrl, logProvidedUrls = false }) {
|
|
35
|
+
const config = new ConfigFile(configPath);
|
|
36
|
+
const configData = config.read();
|
|
37
|
+
const env = envSchema.parse(process.env);
|
|
38
|
+
const givenApiUrl = apiUrl ?? env.PRIVIDIUM_API_URL ?? configData.apiUrl;
|
|
39
|
+
const { url: resolvedApiUrl, prompted } = await askForUrl(givenApiUrl, 'Prividium™ API', logProvidedUrls);
|
|
40
|
+
if (prompted) {
|
|
41
|
+
const confirmation = await confirm({
|
|
42
|
+
message: 'Do you want to store this config for future usages?'
|
|
43
|
+
});
|
|
44
|
+
if (confirmation) {
|
|
45
|
+
config.write({ apiUrl: resolvedApiUrl });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return resolvedApiUrl;
|
|
49
|
+
}
|
|
33
50
|
export async function gatherUrlConfig({ configPath, rpcUrl, userPanelUrl, rpcUrlLabel, logProvidedUrls = false }) {
|
|
34
51
|
const config = new ConfigFile(configPath);
|
|
35
52
|
const configData = config.read();
|
|
36
53
|
const env = envSchema.parse(process.env);
|
|
37
|
-
const givenRpcUrl = rpcUrl ?? env.PRIVIDIUM_RPC_URL ?? configData.
|
|
38
|
-
const givenUserPanelUrl = userPanelUrl ?? env.USER_PANEL_URL
|
|
54
|
+
const givenRpcUrl = rpcUrl ?? env.PRIVIDIUM_API_URL ?? env.PRIVIDIUM_RPC_URL ?? configData.apiUrl;
|
|
55
|
+
const givenUserPanelUrl = userPanelUrl ?? env.USER_PANEL_URL;
|
|
39
56
|
const { url: prividiumRpcUrl, prompted: promptedRpc } = await askForUrl(givenRpcUrl, rpcUrlLabel, logProvidedUrls);
|
|
40
57
|
const { url: resolvedUserPanelUrl, prompted: promptedPanel } = await askForUrl(givenUserPanelUrl, 'user panel', logProvidedUrls);
|
|
41
58
|
if (promptedRpc || promptedPanel) {
|
|
@@ -43,7 +60,7 @@ export async function gatherUrlConfig({ configPath, rpcUrl, userPanelUrl, rpcUrl
|
|
|
43
60
|
message: 'Do you want to store this config for future usages?'
|
|
44
61
|
});
|
|
45
62
|
if (confirmation) {
|
|
46
|
-
config.write({
|
|
63
|
+
config.write({ apiUrl: prividiumRpcUrl });
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
66
|
return { prividiumRpcUrl, userPanelUrl: resolvedUserPanelUrl };
|
|
@@ -7,10 +7,30 @@ import { z } from 'zod';
|
|
|
7
7
|
// biome-ignore lint/suspicious/noExplicitAny: CJS/ESM interop
|
|
8
8
|
const appDirsFn = appDirs.default;
|
|
9
9
|
const dirs = appDirsFn({ appName: 'prividium-proxy' });
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// Source of truth for the on-disk config-file shapes the SDK accepts.
|
|
11
|
+
// `latest` is what `write()` produces today.
|
|
12
|
+
// Any other entry is keyed by the last SDK version that wrote that shape;
|
|
13
|
+
// it exists only so older config files keep working after a user upgrades.
|
|
14
|
+
// To add a new legacy entry: copy the previous `latest` into a new `vX_Y`
|
|
15
|
+
// key, then change `latest` to the new shape and add a branch in `migrate()`.
|
|
16
|
+
const schemas = {
|
|
17
|
+
latest: z.object({
|
|
18
|
+
apiUrl: z.string()
|
|
19
|
+
}),
|
|
20
|
+
v0_17: z.object({
|
|
21
|
+
prividiumRpcUrl: z.string(),
|
|
22
|
+
userPanelUrl: z.string()
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
function migrate(json) {
|
|
26
|
+
const latest = schemas.latest.safeParse(json);
|
|
27
|
+
if (latest.success)
|
|
28
|
+
return latest.data;
|
|
29
|
+
const v0_17 = schemas.v0_17.safeParse(json);
|
|
30
|
+
if (v0_17.success)
|
|
31
|
+
return { apiUrl: v0_17.data.prividiumRpcUrl };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
14
34
|
export class ConfigFile {
|
|
15
35
|
filePath;
|
|
16
36
|
constructor(filePath) {
|
|
@@ -28,7 +48,8 @@ export class ConfigFile {
|
|
|
28
48
|
else {
|
|
29
49
|
const data = readFileSync(this.filePath).toString();
|
|
30
50
|
try {
|
|
31
|
-
|
|
51
|
+
const json = JSON.parse(data);
|
|
52
|
+
return migrate(json) ?? {};
|
|
32
53
|
}
|
|
33
54
|
catch {
|
|
34
55
|
rmSync(this.filePath);
|
|
@@ -48,9 +69,8 @@ export class ConfigFile {
|
|
|
48
69
|
}
|
|
49
70
|
}
|
|
50
71
|
print() {
|
|
51
|
-
const {
|
|
52
|
-
console.log(`${color.bold('Prividium™
|
|
53
|
-
console.log(`${color.bold('Log in url')}: ${userPanelUrl}`);
|
|
72
|
+
const { apiUrl } = this.read();
|
|
73
|
+
console.log(`${color.bold('Prividium™ API url')}: ${apiUrl}`);
|
|
54
74
|
}
|
|
55
75
|
printPath() {
|
|
56
76
|
console.log(this.filePath);
|
|
@@ -48,7 +48,7 @@ export class CreationWorkflow {
|
|
|
48
48
|
this.submitCallback('Authentication successful!');
|
|
49
49
|
await setTimeout(100);
|
|
50
50
|
}
|
|
51
|
-
log.info(`Your proxy rpc is ready! 🚀: \n\n${color.bold(`http://${this.host}:${this.port}
|
|
51
|
+
log.info(`Your proxy rpc is ready! 🚀: \n\n${color.bold(`http://${this.host}:${this.port}`)}\n`);
|
|
52
52
|
log.info('Waiting for logs...');
|
|
53
53
|
}
|
|
54
54
|
onMessage(msg) {
|
|
@@ -29,8 +29,9 @@ export function buildServer(config) {
|
|
|
29
29
|
return reply.status(500).send({ error: err.message });
|
|
30
30
|
});
|
|
31
31
|
const state = randomStateString();
|
|
32
|
-
let accessToken = '';
|
|
33
|
-
let expiresAt = new Date();
|
|
32
|
+
let accessToken = config.initialToken?.accessToken ?? '';
|
|
33
|
+
let expiresAt = config.initialToken?.expiresAt ?? new Date();
|
|
34
|
+
let reauthPromise = null;
|
|
34
35
|
app.register(fastifyStatic, { root: path.join(import.meta.dirname, '..', 'static') });
|
|
35
36
|
app.get('/health', async (_req, reply) => {
|
|
36
37
|
return reply.send('ok');
|
|
@@ -40,7 +41,7 @@ export function buildServer(config) {
|
|
|
40
41
|
return reply.sendFile(path.join('start.html'));
|
|
41
42
|
});
|
|
42
43
|
app.get('/redirect-uri', async (_req, reply) => {
|
|
43
|
-
const url = new URL('/auth/authorize', config.userPanelUrl);
|
|
44
|
+
const url = new URL('/auth/authorize', config.userPanelUrl ?? config.apiUrl);
|
|
44
45
|
url.searchParams.set('client_id', 'proxy-cli');
|
|
45
46
|
url.searchParams.set('redirect_uri', `http://localhost:24101/callback`);
|
|
46
47
|
url.searchParams.set('state', state);
|
|
@@ -62,7 +63,7 @@ export function buildServer(config) {
|
|
|
62
63
|
if (req.body.state !== state) {
|
|
63
64
|
throw new Error('invalid state received');
|
|
64
65
|
}
|
|
65
|
-
const { expiresAt: expirationMoment } = await fetch(
|
|
66
|
+
const { expiresAt: expirationMoment } = await fetch(new URL('/api/auth/current-session', config.apiUrl), {
|
|
66
67
|
method: 'GET',
|
|
67
68
|
headers: { authorization: `Bearer ${req.body.token}` }
|
|
68
69
|
}).then((res) => {
|
|
@@ -82,18 +83,35 @@ export function buildServer(config) {
|
|
|
82
83
|
await config.onSubmit();
|
|
83
84
|
return reply.send('ok');
|
|
84
85
|
});
|
|
85
|
-
|
|
86
|
-
upstream: config.
|
|
87
|
-
prefix
|
|
86
|
+
const proxyOptions = (prefix) => ({
|
|
87
|
+
upstream: config.apiUrl,
|
|
88
|
+
prefix,
|
|
88
89
|
rewritePrefix: '/rpc',
|
|
89
90
|
httpMethods: ['POST'],
|
|
90
|
-
|
|
91
|
-
preValidation: (request, _reply, done) => {
|
|
91
|
+
preValidation: async (request) => {
|
|
92
92
|
const method = rpcReqSchema.safeParse(request.body);
|
|
93
93
|
if (new Date() > expiresAt) {
|
|
94
|
-
config.
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
if (config.onReAuth) {
|
|
95
|
+
try {
|
|
96
|
+
if (!reauthPromise) {
|
|
97
|
+
reauthPromise = config.onReAuth().finally(() => {
|
|
98
|
+
reauthPromise = null;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const newToken = await reauthPromise;
|
|
102
|
+
accessToken = newToken.accessToken;
|
|
103
|
+
expiresAt = newToken.expiresAt;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
107
|
+
config.onError(new Error(`Re-authentication failed: ${msg}`));
|
|
108
|
+
throw new Error('Session expired and re-authentication failed. Please restart the proxy.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
config.onReAuthNeeded();
|
|
113
|
+
throw new Error('Token expired');
|
|
114
|
+
}
|
|
97
115
|
}
|
|
98
116
|
if (!method.success) {
|
|
99
117
|
config.onCall('unknown');
|
|
@@ -104,7 +122,6 @@ export function buildServer(config) {
|
|
|
104
122
|
else {
|
|
105
123
|
config.onCall(method.data.method);
|
|
106
124
|
}
|
|
107
|
-
done();
|
|
108
125
|
},
|
|
109
126
|
preHandler: (req, reply, done) => {
|
|
110
127
|
if (accessToken === '') {
|
|
@@ -127,5 +144,7 @@ export function buildServer(config) {
|
|
|
127
144
|
})
|
|
128
145
|
}
|
|
129
146
|
});
|
|
147
|
+
app.register(fastifyHttpProxy, proxyOptions('/'));
|
|
148
|
+
app.register(fastifyHttpProxy, proxyOptions('/rpc'));
|
|
130
149
|
return app;
|
|
131
150
|
}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<div id="proxy-url" style="display: none" class="mt-6 text-blue-700 text-center">
|
|
46
46
|
<img src="https://www.zksync.io/faq/faq-brackets.svg" alt="Success" class="h-12 w-auto mx-auto mb-3" />
|
|
47
47
|
<span>You can now access your Prividium™ RPC proxy at: </span>
|
|
48
|
-
<span class="font-bold">http://127.0.0.1:24101
|
|
48
|
+
<span class="font-bold">http://127.0.0.1:24101</span>
|
|
49
49
|
</div>
|
|
50
50
|
<pre
|
|
51
51
|
id="error-details"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { UNAUTHORIZED_ERROR_CODE } from './rpc-error-codes.js';
|
|
3
|
+
const jsonRpcErrorSchema = z.object({
|
|
4
|
+
error: z.object({
|
|
5
|
+
code: z.number(),
|
|
6
|
+
message: z.string().optional(),
|
|
7
|
+
data: z.unknown().optional()
|
|
8
|
+
})
|
|
9
|
+
});
|
|
10
|
+
const apiErrorSchema = z.object({
|
|
11
|
+
error: z.object({
|
|
12
|
+
code: z.string(),
|
|
13
|
+
message: z.string()
|
|
14
|
+
})
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Checks if a Response contains a Prividium unauthorized/forbidden error.
|
|
18
|
+
*/
|
|
19
|
+
export async function hasPrividiumUnauthorizedError(response) {
|
|
20
|
+
try {
|
|
21
|
+
const clonedResponse = response.clone();
|
|
22
|
+
const parsed = jsonRpcErrorSchema.safeParse(await clonedResponse.json());
|
|
23
|
+
if (parsed.success) {
|
|
24
|
+
return parsed.data.error.code === UNAUTHORIZED_ERROR_CODE;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function isPrividiumUnauthorizedRpcError(error) {
|
|
33
|
+
if (!error || typeof error !== 'object') {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const value = error;
|
|
37
|
+
if (value.code === UNAUTHORIZED_ERROR_CODE) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return isPrividiumUnauthorizedRpcError(value.cause);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Extracts a human-readable error string from a failed HTTP response.
|
|
44
|
+
* Attempts to parse the server's `{ error: { code, message } }` JSON structure,
|
|
45
|
+
* falls back to plain text, and ultimately to status + statusText.
|
|
46
|
+
* Never throws — always returns a usable string.
|
|
47
|
+
*/
|
|
48
|
+
export async function extractResponseError(response) {
|
|
49
|
+
const base = `${response.status} ${response.statusText}`;
|
|
50
|
+
try {
|
|
51
|
+
const cloned = response.clone();
|
|
52
|
+
const text = await cloned.text();
|
|
53
|
+
if (!text) {
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const json = JSON.parse(text);
|
|
58
|
+
const parsed = apiErrorSchema.safeParse(json);
|
|
59
|
+
if (parsed.success) {
|
|
60
|
+
const { code, message } = parsed.data.error;
|
|
61
|
+
return `${base}: ${message} (${code})`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Not JSON
|
|
66
|
+
}
|
|
67
|
+
return `${base}: ${text}`;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Body unreadable
|
|
71
|
+
}
|
|
72
|
+
return base;
|
|
73
|
+
}
|