prividium 0.10.1 → 0.12.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.
Files changed (32) hide show
  1. package/dist/cli/commands/doctor/clients/browser-auth.js +106 -0
  2. package/dist/cli/commands/doctor/clients/http.js +19 -0
  3. package/dist/cli/commands/doctor/clients/rpc.js +60 -0
  4. package/dist/cli/commands/doctor/clients/wallet-api.js +38 -0
  5. package/dist/cli/commands/doctor/constants.js +1 -0
  6. package/dist/cli/commands/doctor/probes/authentication/authentication.js +27 -0
  7. package/dist/cli/commands/doctor/probes/authentication/wallet-preconditions.js +8 -0
  8. package/dist/cli/commands/doctor/probes/authentication.js +35 -0
  9. package/dist/cli/commands/doctor/probes/bridging/bridging.js +43 -0
  10. package/dist/cli/commands/doctor/probes/bridging.js +32 -0
  11. package/dist/cli/commands/doctor/probes/global/input-validation.js +20 -0
  12. package/dist/cli/commands/doctor/probes/global/reachability.js +28 -0
  13. package/dist/cli/commands/doctor/probes/global.js +47 -0
  14. package/dist/cli/commands/doctor/probes/wallet/authenticated-rpc.js +18 -0
  15. package/dist/cli/commands/doctor/probes/wallet/authorization-and-wallet-rpc.js +33 -0
  16. package/dist/cli/commands/doctor/probes/wallet-api.js +48 -0
  17. package/dist/cli/commands/doctor/probes/wallet.js +49 -0
  18. package/dist/cli/commands/doctor/profile.js +9 -0
  19. package/dist/cli/commands/doctor/report/build.js +79 -0
  20. package/dist/cli/commands/doctor/report/render.js +66 -0
  21. package/dist/cli/commands/doctor/stages.js +50 -0
  22. package/dist/cli/commands/doctor/types.js +1 -0
  23. package/dist/cli/commands/doctor/utils.js +52 -0
  24. package/dist/cli/commands/doctor.js +77 -0
  25. package/dist/cli/commands/proxy.js +26 -21
  26. package/dist/cli/commands/utils/show-prividium-header.js +11 -0
  27. package/dist/cli/commands/utils/url-config.js +50 -0
  28. package/dist/cli/index.js +2 -1
  29. package/dist/cli/server/connection-workflow.js +4 -56
  30. package/dist/sdk/siwe-auth.js +3 -2
  31. package/dist/tsconfig.cli.tsbuildinfo +1 -1
  32. package/package.json +1 -1
@@ -0,0 +1,106 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { confirm, log } from '@clack/prompts';
4
+ import fastifyStatic from '@fastify/static';
5
+ import Fastify from 'fastify';
6
+ import { validatorCompiler } from 'fastify-type-provider-zod';
7
+ import open from 'open';
8
+ import { z } from 'zod';
9
+ const AUTH_BROWSER_TIMEOUT_MS = 30_000;
10
+ export async function authenticateInBrowser(userPanelBaseUrl, callbackPort) {
11
+ const state = randomBytes(20).toString('hex');
12
+ const app = Fastify().withTypeProvider();
13
+ app.setValidatorCompiler(validatorCompiler);
14
+ app.register(fastifyStatic, { root: path.join(import.meta.dirname, '..', '..', '..', 'static') });
15
+ let resolveToken;
16
+ let rejectToken;
17
+ const tokenPromise = new Promise((resolve, reject) => {
18
+ resolveToken = resolve;
19
+ rejectToken = reject;
20
+ });
21
+ app.get('/', async (_req, reply) => {
22
+ reply.header('Cache-Control', 'no-cache');
23
+ return reply.type('text/html').send(`<!doctype html>
24
+ <html lang="en">
25
+ <head>
26
+ <meta charset="UTF-8" />
27
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
28
+ <title>Prividium Doctor</title>
29
+ </head>
30
+ <body>
31
+ <script>
32
+ fetch('/redirect-uri')
33
+ .then((response) => response.text())
34
+ .then((url) => {
35
+ window.location.assign(url);
36
+ })
37
+ .catch((error) => {
38
+ document.body.innerText = error.message;
39
+ });
40
+ </script>
41
+ </body>
42
+ </html>`);
43
+ });
44
+ app.get('/redirect-uri', async (_req, reply) => {
45
+ const authUrl = new URL('/auth/authorize', userPanelBaseUrl);
46
+ authUrl.searchParams.set('client_id', 'proxy-cli');
47
+ authUrl.searchParams.set('redirect_uri', `http://localhost:${callbackPort}/callback`);
48
+ authUrl.searchParams.set('state', state);
49
+ authUrl.searchParams.set('response_type', 'token');
50
+ return reply.send(authUrl.toString());
51
+ });
52
+ app.get('/callback', async (_req, reply) => {
53
+ reply.header('Cache-Control', 'no-cache');
54
+ return reply.sendFile(path.join('callback.html'));
55
+ });
56
+ app.post('/submit', {
57
+ schema: {
58
+ body: z.object({
59
+ token: z.string(),
60
+ state: z.string()
61
+ })
62
+ }
63
+ }, async (req, reply) => {
64
+ if (req.body.state !== state) {
65
+ return reply.status(400).send('invalid state received');
66
+ }
67
+ resolveToken(req.body.token);
68
+ return reply.send('ok');
69
+ });
70
+ app.setErrorHandler((error, _req, reply) => {
71
+ rejectToken(error instanceof Error ? error : new Error(String(error)));
72
+ return reply.status(500).send(error instanceof Error ? error.message : String(error));
73
+ });
74
+ try {
75
+ await app.listen({ host: 'localhost', port: callbackPort });
76
+ }
77
+ catch (error) {
78
+ await app.close();
79
+ if (error instanceof Error && error.message.includes('EADDRINUSE')) {
80
+ throw new Error(`port ${callbackPort} already in use — close other Prividium proxy or doctor instances and retry`);
81
+ }
82
+ throw error;
83
+ }
84
+ try {
85
+ const startUrl = `http://localhost:${callbackPort}/`;
86
+ await open(startUrl);
87
+ }
88
+ catch {
89
+ log.warn(`Browser did not open automatically. Open http://localhost:${callbackPort}/ manually.`);
90
+ }
91
+ const timeoutId = setTimeout(async () => {
92
+ const stillWaiting = await confirm({
93
+ message: 'Browser login is taking a while. Still completing login?'
94
+ });
95
+ if (stillWaiting !== true) {
96
+ rejectToken(new Error('Authentication canceled by user'));
97
+ }
98
+ }, AUTH_BROWSER_TIMEOUT_MS);
99
+ try {
100
+ return await tokenPromise;
101
+ }
102
+ finally {
103
+ clearTimeout(timeoutId);
104
+ await app.close();
105
+ }
106
+ }
@@ -0,0 +1,19 @@
1
+ import { getRawReason } from '../utils.js';
2
+ export async function requireHttpOk(url) {
3
+ const response = await fetch(url);
4
+ if (!response.ok) {
5
+ const reason = getRawReason(response.status, await response.text());
6
+ throw new Error(reason);
7
+ }
8
+ }
9
+ export async function fetchAuthenticatedJson(schema, url, token) {
10
+ const response = await fetch(url, {
11
+ headers: {
12
+ authorization: `Bearer ${token}`
13
+ }
14
+ });
15
+ if (!response.ok) {
16
+ throw new Error(getRawReason(response.status, await response.text()));
17
+ }
18
+ return schema.parse(await response.json());
19
+ }
@@ -0,0 +1,60 @@
1
+ import { getThrownReason } from '../utils.js';
2
+ export async function callRpc(apiBaseUrl, { method, params = [], path = '/rpc', token }) {
3
+ try {
4
+ const response = await fetch(new URL(path, apiBaseUrl), {
5
+ method: 'POST',
6
+ headers: {
7
+ 'content-type': 'application/json',
8
+ ...(token ? { authorization: `Bearer ${token}` } : {})
9
+ },
10
+ body: JSON.stringify({
11
+ id: method,
12
+ jsonrpc: '2.0',
13
+ method,
14
+ params
15
+ })
16
+ });
17
+ if (!response.ok) {
18
+ return {
19
+ error: {
20
+ code: response.status,
21
+ message: await response.text()
22
+ }
23
+ };
24
+ }
25
+ return (await response.json());
26
+ }
27
+ catch (error) {
28
+ return {
29
+ error: {
30
+ code: -1,
31
+ message: getThrownReason(error)
32
+ }
33
+ };
34
+ }
35
+ }
36
+ export async function requireRpcResult(apiBaseUrl, options) {
37
+ const rpcResponse = await callRpc(apiBaseUrl, options);
38
+ if (rpcResponse.error || rpcResponse.result === undefined) {
39
+ throw new Error(formatRpcError(options.method, rpcResponse));
40
+ }
41
+ return rpcResponse.result;
42
+ }
43
+ export async function requireForbiddenRpc(apiBaseUrl, options) {
44
+ const rpcResponse = await callRpc(apiBaseUrl, options);
45
+ if (!rpcResponse.error) {
46
+ throw new Error(`RPC method unexpectedly allowed on ${options.method}`);
47
+ }
48
+ if (!isForbiddenRpc(rpcResponse)) {
49
+ throw new Error(formatRpcError(options.method, rpcResponse));
50
+ }
51
+ }
52
+ export function formatRpcError(method, rpcResponse) {
53
+ if (rpcResponse.error) {
54
+ return `RPC ${rpcResponse.error.code}: ${rpcResponse.error.message} on ${method}`;
55
+ }
56
+ return `RPC unknown: invalid response on ${method}`;
57
+ }
58
+ export function isForbiddenRpc(rpcResponse) {
59
+ return rpcResponse.error?.code === 403 || rpcResponse.error?.message.toLowerCase().includes('forbidden') || false;
60
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ export async function fetchWalletRpcToken(apiBaseUrl, token) {
3
+ const response = await fetch(new URL('/api/wallet/personal-rpc-token', apiBaseUrl), {
4
+ headers: {
5
+ authorization: `Bearer ${token}`
6
+ }
7
+ });
8
+ if (!response.ok) {
9
+ const body = await response.text();
10
+ throw new Error(body.trim().length > 0 ? `HTTP ${response.status}: ${body.trim()}` : `HTTP ${response.status}`);
11
+ }
12
+ return z.object({ token: z.string() }).parse(await response.json());
13
+ }
14
+ export async function authorizeTransactionProbe(apiBaseUrl, token, walletAddress, nonce) {
15
+ const response = await fetch(new URL('/api/wallet/transaction-authorization', apiBaseUrl), {
16
+ method: 'POST',
17
+ headers: {
18
+ 'content-type': 'application/json',
19
+ authorization: `Bearer ${token}`
20
+ },
21
+ body: JSON.stringify({
22
+ walletAddress,
23
+ nonce: Number(nonce),
24
+ toAddress: walletAddress,
25
+ value: '1'
26
+ })
27
+ });
28
+ if (!response.ok) {
29
+ const body = await response.text();
30
+ throw new Error(body.trim().length > 0 ? `HTTP ${response.status}: ${body.trim()}` : `HTTP ${response.status}`);
31
+ }
32
+ return z
33
+ .object({
34
+ message: z.string(),
35
+ activeUntil: z.string()
36
+ })
37
+ .parse(await response.json());
38
+ }
@@ -0,0 +1 @@
1
+ export const DEFAULT_DOCTOR_CALLBACK_PORT = 24101;
@@ -0,0 +1,27 @@
1
+ import { sessionSchema } from '../../../../server/server.js';
2
+ import { authenticateInBrowser } from '../../clients/browser-auth.js';
3
+ import { fetchAuthenticatedJson } from '../../clients/http.js';
4
+ import { DEFAULT_DOCTOR_CALLBACK_PORT } from '../../constants';
5
+ import { profileSchema } from '../../profile.js';
6
+ export async function runBrowserAuthProbe(context) {
7
+ const token = await authenticateInBrowser(context.targets.userPanelBaseUrl, DEFAULT_DOCTOR_CALLBACK_PORT);
8
+ context.pendingToken = token;
9
+ }
10
+ export async function runSessionValidationProbe(context) {
11
+ const session = await fetchAuthenticatedJson(sessionSchema, new URL('/api/auth/current-session', context.targets.apiBaseUrl).href, context.pendingToken);
12
+ context.pendingSession = session;
13
+ }
14
+ export async function runProfileFetchProbe(context) {
15
+ const profile = await fetchAuthenticatedJson(profileSchema, new URL('/api/profiles/me', context.targets.apiBaseUrl).href, context.pendingToken);
16
+ context.authState = { token: context.pendingToken, session: context.pendingSession, profile };
17
+ context.reportContext = {
18
+ sessionType: context.authState.session.type,
19
+ authExpiresAt: context.authState.session.expiresAt.toISOString(),
20
+ userDisplayName: context.authState.profile.displayName,
21
+ userId: context.authState.profile.id,
22
+ roles: context.authState.profile.roles.map((role) => role.roleName),
23
+ walletCount: context.authState.profile.wallets.length
24
+ };
25
+ context.pendingToken = undefined;
26
+ context.pendingSession = undefined;
27
+ }
@@ -0,0 +1,8 @@
1
+ export async function runWalletPreconditionsProbe(context) {
2
+ const walletAddresses = context.authState.profile.wallets.map((wallet) => wallet.walletAddress);
3
+ context.walletAddresses = walletAddresses;
4
+ if (walletAddresses.length === 0) {
5
+ return { status: 'warn', details: ['No associated wallets'] };
6
+ }
7
+ return { values: [{ label: 'walletCount', value: `wallets: ${String(walletAddresses.length)}`, inline: true }] };
8
+ }
@@ -0,0 +1,35 @@
1
+ import { DEFAULT_DOCTOR_CALLBACK_PORT } from '../constants';
2
+ import { passed } from '../utils.js';
3
+ import { runBrowserAuthProbe, runProfileFetchProbe, runSessionValidationProbe } from './authentication/authentication.js';
4
+ import { runWalletPreconditionsProbe } from './authentication/wallet-preconditions.js';
5
+ export const authenticationStage = {
6
+ id: 'authentication',
7
+ title: 'Authentication',
8
+ getProbes: () => [
9
+ {
10
+ id: 'browser-authentication',
11
+ label: 'Browser authentication',
12
+ progressMessage: `Complete sign-in in the browser. If needed, open http://localhost:${DEFAULT_DOCTOR_CALLBACK_PORT}/`,
13
+ runIf: passed(['user-panel-reachable', 'User Panel not reachable'], ['api-health', 'API not reachable'], ['callback-port-available', 'Callback port not available']),
14
+ run: runBrowserAuthProbe
15
+ },
16
+ {
17
+ id: 'session-validation',
18
+ label: 'Session validation',
19
+ runIf: passed('browser-authentication', 'Browser authentication failed'),
20
+ run: runSessionValidationProbe
21
+ },
22
+ {
23
+ id: 'profile-fetch',
24
+ label: 'Profile fetch',
25
+ runIf: passed('session-validation', 'Session validation failed'),
26
+ run: runProfileFetchProbe
27
+ },
28
+ {
29
+ id: 'wallet-preconditions',
30
+ label: 'Wallet preconditions',
31
+ runIf: passed('profile-fetch', 'Profile not available'),
32
+ run: runWalletPreconditionsProbe
33
+ }
34
+ ]
35
+ };
@@ -0,0 +1,43 @@
1
+ import { decodeFunctionResult, encodeFunctionData, parseAbiItem } from 'viem';
2
+ import { requireRpcResult } from '../../clients/rpc.js';
3
+ const DOCTOR_DIAGNOSTIC_CONTRACT = '0x0000000000000000000000000000000000010004';
4
+ const doctorDiagnosticAbi = [
5
+ parseAbiItem('function L1_CHAIN_ID() view returns (uint256)'),
6
+ parseAbiItem('function BASE_TOKEN_ASSET_ID() view returns (bytes32)'),
7
+ parseAbiItem('function WETH_TOKEN() view returns (address)'),
8
+ parseAbiItem('function originChainId(bytes32 assetId) view returns (uint256)')
9
+ ];
10
+ export async function runBridgingL1ChainIdProbe(ctx, walletAddress) {
11
+ const value = await readContract(ctx, walletAddress, 'L1_CHAIN_ID');
12
+ return { values: [{ label: 'l1ChainId', value: String(value), inline: true }] };
13
+ }
14
+ export async function runBridgingBaseTokenProbe(ctx, walletAddress) {
15
+ const value = await readContract(ctx, walletAddress, 'BASE_TOKEN_ASSET_ID');
16
+ return { values: [{ label: 'baseTokenAssetId', value: String(value), inline: true }] };
17
+ }
18
+ export async function runBridgingWethTokenProbe(ctx, walletAddress) {
19
+ const value = await readContract(ctx, walletAddress, 'WETH_TOKEN');
20
+ return { values: [{ label: 'wethToken', value: String(value), inline: true }] };
21
+ }
22
+ export async function runBridgingOriginChainIdProbe(ctx, walletAddress) {
23
+ const baseTokenAssetId = ctx.results
24
+ .find((r) => r.id === `bridging-base-token:${walletAddress}`)
25
+ ?.values?.find((v) => v.label === 'baseTokenAssetId')?.value;
26
+ const value = await readContract(ctx, walletAddress, 'originChainId', [baseTokenAssetId]);
27
+ return { values: [{ label: 'originChainId', value: String(value), inline: true }] };
28
+ }
29
+ async function readContract(ctx, walletAddress, functionName, args = []) {
30
+ const result = await requireRpcResult(ctx.targets.apiBaseUrl, {
31
+ method: 'eth_call',
32
+ token: ctx.authState.token,
33
+ params: [
34
+ {
35
+ to: DOCTOR_DIAGNOSTIC_CONTRACT,
36
+ from: walletAddress,
37
+ data: encodeFunctionData({ abi: doctorDiagnosticAbi, functionName, args: args })
38
+ },
39
+ 'latest'
40
+ ]
41
+ });
42
+ return decodeFunctionResult({ abi: doctorDiagnosticAbi, functionName, data: result });
43
+ }
@@ -0,0 +1,32 @@
1
+ import { passed, shortenAddress } from '../utils.js';
2
+ import { runBridgingBaseTokenProbe, runBridgingL1ChainIdProbe, runBridgingOriginChainIdProbe, runBridgingWethTokenProbe } from './bridging/bridging.js';
3
+ export function createBridgingStage(walletAddress) {
4
+ return {
5
+ id: 'bridging',
6
+ title: `Bridging · (using: ${shortenAddress(walletAddress)})`,
7
+ getProbes: () => [
8
+ {
9
+ id: `bridging-l1-chain-id:${walletAddress}`,
10
+ label: 'L1 chain ID',
11
+ progressMessage: `Bridging checks: ${walletAddress}`,
12
+ run: (ctx) => runBridgingL1ChainIdProbe(ctx, walletAddress)
13
+ },
14
+ {
15
+ id: `bridging-base-token:${walletAddress}`,
16
+ label: 'Base token asset ID',
17
+ run: (ctx) => runBridgingBaseTokenProbe(ctx, walletAddress)
18
+ },
19
+ {
20
+ id: `bridging-weth:${walletAddress}`,
21
+ label: 'WETH token',
22
+ run: (ctx) => runBridgingWethTokenProbe(ctx, walletAddress)
23
+ },
24
+ {
25
+ id: `bridging-origin-chain-id:${walletAddress}`,
26
+ label: 'Origin chain ID',
27
+ runIf: passed(`bridging-base-token:${walletAddress}`, 'BASE_TOKEN_ASSET_ID check did not pass'),
28
+ run: (ctx) => runBridgingOriginChainIdProbe(ctx, walletAddress)
29
+ }
30
+ ]
31
+ };
32
+ }
@@ -0,0 +1,20 @@
1
+ import { validateAndNormalizeUrls } from '../../utils.js';
2
+ export async function runInputValidationProbe(context) {
3
+ const targets = validateAndNormalizeUrls(context.rawRpcUrl, context.rawUserPanelUrl);
4
+ context.targets = targets;
5
+ }
6
+ export async function runHostMismatchProbe(context) {
7
+ const warnings = [];
8
+ if (context.targets.apiBaseUrl !== context.rawRpcUrl) {
9
+ warnings.push({ label: 'api', value: context.rawRpcUrl });
10
+ }
11
+ if (context.targets.userPanelBaseUrl !== context.rawUserPanelUrl) {
12
+ warnings.push({ label: 'user panel', value: context.rawUserPanelUrl });
13
+ }
14
+ if (warnings.length > 0) {
15
+ return {
16
+ status: 'warn',
17
+ values: warnings
18
+ };
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ import * as net from 'node:net';
2
+ import { requireHttpOk } from '../../clients/http.js';
3
+ import { requireRpcResult } from '../../clients/rpc.js';
4
+ import { DEFAULT_DOCTOR_CALLBACK_PORT } from '../../constants.js';
5
+ export async function runCallbackPortProbe() {
6
+ const available = await new Promise((resolve) => {
7
+ const server = net.createServer();
8
+ server.once('error', () => resolve(false));
9
+ server.once('listening', () => server.close(() => resolve(true)));
10
+ server.listen(DEFAULT_DOCTOR_CALLBACK_PORT, '127.0.0.1');
11
+ });
12
+ if (!available) {
13
+ throw new Error(`Port ${DEFAULT_DOCTOR_CALLBACK_PORT} is already in use. Stop Prividium™ Proxy or other conflicting process before running doctor`);
14
+ }
15
+ }
16
+ export async function runUserPanelReachabilityProbe(context) {
17
+ await requireHttpOk(new URL('/health', context.targets.userPanelBaseUrl));
18
+ }
19
+ export async function runApiHealthProbe(context) {
20
+ await requireHttpOk(new URL('/health', context.targets.apiBaseUrl));
21
+ await requireHttpOk(new URL('/readyz', context.targets.apiBaseUrl));
22
+ }
23
+ export async function runPublicRpcProbe(context) {
24
+ const version = await requireRpcResult(context.targets.apiBaseUrl, { method: 'web3_clientVersion' });
25
+ return {
26
+ values: [{ label: 'blockNumber', value: version, inline: true }]
27
+ };
28
+ }
@@ -0,0 +1,47 @@
1
+ import { passed } from '../utils.js';
2
+ import { runHostMismatchProbe, runInputValidationProbe } from './global/input-validation.js';
3
+ import { runApiHealthProbe, runCallbackPortProbe, runPublicRpcProbe, runUserPanelReachabilityProbe } from './global/reachability.js';
4
+ export const globalStage = {
5
+ id: 'global',
6
+ title: 'Global',
7
+ getProbes: () => [
8
+ {
9
+ id: 'input-validation',
10
+ label: 'Input Validation',
11
+ progressMessage: 'Validating input URLs',
12
+ run: runInputValidationProbe
13
+ },
14
+ {
15
+ id: 'callback-port-available',
16
+ label: 'Callback port available',
17
+ run: runCallbackPortProbe
18
+ },
19
+ {
20
+ id: 'host-mismatch',
21
+ label: 'Host validation',
22
+ runIf: passed('input-validation', 'Input validation failed'),
23
+ run: runHostMismatchProbe
24
+ },
25
+ {
26
+ id: 'user-panel-reachable',
27
+ label: 'User Panel reachability',
28
+ progressMessage: 'Checking User Panel reachability',
29
+ runIf: passed('input-validation', 'Input validation failed'),
30
+ run: runUserPanelReachabilityProbe
31
+ },
32
+ {
33
+ id: 'api-health',
34
+ label: 'API health',
35
+ progressMessage: 'Checking API health',
36
+ runIf: passed('input-validation', 'Input validation failed'),
37
+ run: runApiHealthProbe
38
+ },
39
+ {
40
+ id: 'public-rpc',
41
+ label: 'Public RPC',
42
+ progressMessage: 'Checking public RPC',
43
+ runIf: passed('input-validation', 'Input validation failed'),
44
+ run: runPublicRpcProbe
45
+ }
46
+ ]
47
+ };
@@ -0,0 +1,18 @@
1
+ import { requireRpcResult } from '../../clients/rpc.js';
2
+ import { formatHexQuantity } from '../../utils.js';
3
+ export async function runAuthBalanceProbe(context, walletAddress) {
4
+ const balance = await requireRpcResult(context.targets.apiBaseUrl, {
5
+ method: 'eth_getBalance',
6
+ token: context.authState.token,
7
+ params: [walletAddress, 'latest']
8
+ });
9
+ return { values: [{ label: 'balance', value: formatHexQuantity(balance), inline: true }] };
10
+ }
11
+ export async function runAuthNonceProbe(context, walletAddress) {
12
+ const nonce = await requireRpcResult(context.targets.apiBaseUrl, {
13
+ method: 'eth_getTransactionCount',
14
+ token: context.authState.token,
15
+ params: [walletAddress, 'latest']
16
+ });
17
+ return { values: [{ label: 'nonce', value: formatHexQuantity(nonce), inline: true }] };
18
+ }
@@ -0,0 +1,33 @@
1
+ import { requireForbiddenRpc, requireRpcResult } from '../../clients/rpc.js';
2
+ import { authorizeTransactionProbe } from '../../clients/wallet-api.js';
3
+ import { formatHexQuantity } from '../../utils.js';
4
+ export async function runTransactionAuthorizationProbe(context, walletAddress) {
5
+ const nonce = await requireRpcResult(context.targets.apiBaseUrl, {
6
+ method: 'eth_getTransactionCount',
7
+ token: context.authState.token,
8
+ params: [walletAddress, 'latest']
9
+ });
10
+ await authorizeTransactionProbe(context.targets.apiBaseUrl, context.authState.token, walletAddress, BigInt(nonce));
11
+ }
12
+ export async function runWalletBalanceProbe(context, walletAddress) {
13
+ const balance = await requireRpcResult(context.targets.apiBaseUrl, {
14
+ method: 'eth_getBalance',
15
+ path: context.walletApiPath,
16
+ params: [walletAddress, 'latest']
17
+ });
18
+ return { values: [{ label: 'balance', value: formatHexQuantity(balance), inline: true }] };
19
+ }
20
+ export async function runWalletEthCallRestrictionProbe(context, walletAddress) {
21
+ await requireForbiddenRpc(context.targets.apiBaseUrl, {
22
+ method: 'eth_call',
23
+ path: context.walletApiPath,
24
+ params: [{ to: walletAddress }, 'latest']
25
+ });
26
+ }
27
+ export async function runWalletTxCountRestrictionProbe(context, walletAddress) {
28
+ await requireForbiddenRpc(context.targets.apiBaseUrl, {
29
+ method: 'eth_getTransactionCount',
30
+ path: context.walletApiPath,
31
+ params: [walletAddress, 'latest']
32
+ });
33
+ }
@@ -0,0 +1,48 @@
1
+ import { requireRpcResult } from '../clients/rpc';
2
+ import { fetchWalletRpcToken } from '../clients/wallet-api';
3
+ import { formatHexQuantity, passed } from '../utils';
4
+ export const walletApiStage = {
5
+ id: 'wallet-api',
6
+ title: 'Wallet API',
7
+ getProbes: () => [
8
+ {
9
+ id: 'wallet-api-token',
10
+ label: 'Wallet API token',
11
+ progressMessage: 'Requesting personal wallet RPC token',
12
+ runIf: passed('profile-fetch', 'Authentication not completed'),
13
+ run: async (context) => {
14
+ const { token } = await fetchWalletRpcToken(context.targets.apiBaseUrl, context.authState.token);
15
+ context.walletApiAvailable = true;
16
+ context.walletApiPath = `/rpc/wallet/${token}`;
17
+ }
18
+ },
19
+ {
20
+ id: 'wallet-rpc-chain-id',
21
+ label: 'Wallet RPC eth_chainId',
22
+ runIf: passed('wallet-api-token', 'Wallet API token not available'),
23
+ run: async (context) => {
24
+ const chainId = await requireRpcResult(context.targets.apiBaseUrl, {
25
+ method: 'eth_chainId',
26
+ path: context.walletApiPath
27
+ });
28
+ return {
29
+ values: [{ label: 'blockNumber', value: formatHexQuantity(chainId), inline: true }]
30
+ };
31
+ }
32
+ },
33
+ {
34
+ id: 'wallet-rpc-block-number',
35
+ label: 'Wallet RPC eth_blockNumber',
36
+ runIf: passed('wallet-api-token', 'Wallet API token not available'),
37
+ run: async (context) => {
38
+ const blockNumber = await requireRpcResult(context.targets.apiBaseUrl, {
39
+ method: 'eth_blockNumber',
40
+ path: context.walletApiPath
41
+ });
42
+ return {
43
+ values: [{ label: 'blockNumber', value: formatHexQuantity(blockNumber), inline: true }]
44
+ };
45
+ }
46
+ }
47
+ ]
48
+ };
@@ -0,0 +1,49 @@
1
+ import { passed, shortenAddress } from '../utils';
2
+ import { runAuthBalanceProbe, runAuthNonceProbe } from './wallet/authenticated-rpc.js';
3
+ import { runTransactionAuthorizationProbe, runWalletBalanceProbe, runWalletEthCallRestrictionProbe, runWalletTxCountRestrictionProbe } from './wallet/authorization-and-wallet-rpc.js';
4
+ export function createWalletStage(walletAddress, index, total) {
5
+ const progressSuffix = `${index + 1}/${total}: ${walletAddress}`;
6
+ return {
7
+ id: `wallet:${walletAddress}`,
8
+ title: `Wallet ${index + 1} · ${shortenAddress(walletAddress)}`,
9
+ getProbes: () => [
10
+ {
11
+ id: `authenticated-balance:${walletAddress}`,
12
+ label: 'RPC eth_getBalance',
13
+ progressMessage: `RPC ${progressSuffix}`,
14
+ run: (ctx) => runAuthBalanceProbe(ctx, walletAddress)
15
+ },
16
+ {
17
+ id: `authenticated-nonce:${walletAddress}`,
18
+ label: 'RPC eth_getTransactionCount',
19
+ runIf: passed(`authenticated-balance:${walletAddress}`, 'Authenticated balance check failed'),
20
+ run: (ctx) => runAuthNonceProbe(ctx, walletAddress)
21
+ },
22
+ {
23
+ id: `transaction-authorization:${walletAddress}`,
24
+ label: 'Transaction authorization',
25
+ progressMessage: `Authorization and Wallet RPC ${progressSuffix}`,
26
+ runIf: passed('wallet-api-token', 'Wallet API not available'),
27
+ run: (ctx) => runTransactionAuthorizationProbe(ctx, walletAddress)
28
+ },
29
+ {
30
+ id: `wallet-balance:${walletAddress}`,
31
+ label: 'Wallet RPC eth_getBalance',
32
+ runIf: passed(`transaction-authorization:${walletAddress}`, 'Transaction authorization failed'),
33
+ run: (ctx) => runWalletBalanceProbe(ctx, walletAddress)
34
+ },
35
+ {
36
+ id: `wallet-eth-call-restricted:${walletAddress}`,
37
+ label: 'Wallet RPC eth_call restriction',
38
+ runIf: passed(`wallet-balance:${walletAddress}`, 'Wallet balance check failed'),
39
+ run: (ctx) => runWalletEthCallRestrictionProbe(ctx, walletAddress)
40
+ },
41
+ {
42
+ id: `wallet-eth-get-transaction-count-restricted:${walletAddress}`,
43
+ label: 'Wallet RPC eth_getTransactionCount restriction',
44
+ runIf: passed(`wallet-balance:${walletAddress}`, 'Wallet balance check failed'),
45
+ run: (ctx) => runWalletTxCountRestrictionProbe(ctx, walletAddress)
46
+ }
47
+ ]
48
+ };
49
+ }
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ export const profileSchema = z.object({
3
+ id: z.string(),
4
+ displayName: z.string(),
5
+ roles: z.array(z.object({ roleName: z.string() })),
6
+ wallets: z.array(z.object({
7
+ walletAddress: z.string()
8
+ }))
9
+ });
@@ -0,0 +1,79 @@
1
+ function buildReportStatus(results) {
2
+ if (results.some((result) => result.status === 'fail')) {
3
+ return 'fail';
4
+ }
5
+ if (results.some((result) => result.status === 'warn')) {
6
+ return 'warn';
7
+ }
8
+ if (results.some((result) => result.status === 'pass')) {
9
+ return 'pass';
10
+ }
11
+ return 'skip';
12
+ }
13
+ function buildSummaryBlock(context) {
14
+ const failCount = context.results.filter(({ status }) => status === 'fail').length;
15
+ const skipCount = context.results.filter(({ status }) => status === 'skip').length;
16
+ const passCount = context.results.filter(({ status }) => status === 'pass').length;
17
+ return {
18
+ type: 'values',
19
+ id: 'summary',
20
+ title: 'Summary',
21
+ values: [
22
+ { label: 'Status', value: `${failCount} failed, ${skipCount} skipped, ${passCount} passed` },
23
+ { label: 'API', value: context.targets?.apiBaseUrl ?? context.rawRpcUrl },
24
+ { label: 'User Panel', value: context.targets?.userPanelBaseUrl ?? context.rawUserPanelUrl },
25
+ { label: 'Time', value: new Date().toISOString() }
26
+ ]
27
+ };
28
+ }
29
+ function buildUserContextBlock(context) {
30
+ if (!context.reportContext) {
31
+ return undefined;
32
+ }
33
+ return {
34
+ type: 'values',
35
+ id: 'user-context',
36
+ title: 'User Context',
37
+ values: [
38
+ { label: 'Session type', value: context.reportContext.sessionType },
39
+ {
40
+ label: 'User',
41
+ value: `${context.reportContext.userDisplayName} (${context.reportContext.userId})`
42
+ },
43
+ {
44
+ label: 'Roles',
45
+ value: context.reportContext.roles.length > 0 ? context.reportContext.roles.join(', ') : 'none'
46
+ },
47
+ { label: 'Auth expires', value: context.reportContext.authExpiresAt }
48
+ ]
49
+ };
50
+ }
51
+ function buildSectionBlocks(results, stageTitles) {
52
+ const sectionOrder = [];
53
+ const sectionMap = new Map();
54
+ for (const result of results) {
55
+ if (!sectionMap.has(result.section)) {
56
+ sectionOrder.push(result.section);
57
+ sectionMap.set(result.section, {
58
+ type: 'section',
59
+ id: result.section,
60
+ title: stageTitles.get(result.section) ?? result.section,
61
+ probes: []
62
+ });
63
+ }
64
+ sectionMap.get(result.section).probes.push(result);
65
+ }
66
+ return sectionOrder.map((id) => sectionMap.get(id));
67
+ }
68
+ export function buildDoctorReport(context, stages) {
69
+ const stageTitles = new Map(stages.map((s) => [s.id, s.title]));
70
+ const blocks = [buildSummaryBlock(context), ...buildSectionBlocks(context.results, stageTitles)];
71
+ const userContextBlock = buildUserContextBlock(context);
72
+ if (userContextBlock) {
73
+ blocks.push(userContextBlock);
74
+ }
75
+ return {
76
+ status: buildReportStatus(context.results),
77
+ blocks
78
+ };
79
+ }
@@ -0,0 +1,66 @@
1
+ import { note } from '@clack/prompts';
2
+ import color from 'kleur';
3
+ export function printReport(report) {
4
+ const lines = [];
5
+ for (const block of report.blocks) {
6
+ appendBlock(lines, block);
7
+ }
8
+ note(lines.join('\n'), 'Report');
9
+ }
10
+ function entryIcon(status) {
11
+ if (status === 'pass') {
12
+ return color.bold().green('[OK]');
13
+ }
14
+ if (status === 'warn') {
15
+ return color.bold().yellow('[WARN]');
16
+ }
17
+ if (status === 'skip') {
18
+ return color.bold().cyan('[SKIP]');
19
+ }
20
+ return color.bold().red('[FAIL]');
21
+ }
22
+ function label(text) {
23
+ return color.bold(text);
24
+ }
25
+ function value(text) {
26
+ return color.bold().white(text);
27
+ }
28
+ function formatProbeLines(probe) {
29
+ const lines = [`${entryIcon(probe.status)} ${probe.label}`];
30
+ const inlineValues = probe.values?.filter((entry) => entry.inline) ?? [];
31
+ const inlineValue = inlineValues.length === 1 ? inlineValues[0] : undefined;
32
+ if (inlineValue) {
33
+ lines[0] = `${lines[0]} (${inlineValue.value})`;
34
+ }
35
+ for (const probeValue of probe.values?.filter((entry) => !entry.inline) ?? []) {
36
+ lines.push(` ${probeValue.label}: ${probeValue.value}`);
37
+ }
38
+ for (const detail of probe.details ?? []) {
39
+ if (detail.trim().length > 0) {
40
+ lines.push(` ${detail}`);
41
+ }
42
+ }
43
+ return lines;
44
+ }
45
+ function appendBlock(lines, block) {
46
+ if (block.type === 'values') {
47
+ if (lines.length > 0) {
48
+ lines.push('');
49
+ }
50
+ lines.push(label(block.title));
51
+ for (const blockValue of block.values) {
52
+ lines.push(`${label(`${blockValue.label}:`)} ${value(blockValue.value)}`);
53
+ }
54
+ return;
55
+ }
56
+ if (block.probes.length === 0) {
57
+ return;
58
+ }
59
+ if (lines.length > 0) {
60
+ lines.push('');
61
+ }
62
+ lines.push(label(block.title));
63
+ for (const probe of block.probes) {
64
+ lines.push(...formatProbeLines(probe));
65
+ }
66
+ }
@@ -0,0 +1,50 @@
1
+ import { spinner } from '@clack/prompts';
2
+ import { getThrownReason } from './utils.js';
3
+ export async function executeDoctorStages(context, stages) {
4
+ for (const stage of stages) {
5
+ await runStage(context, stage);
6
+ }
7
+ }
8
+ async function runStage(context, stage) {
9
+ const progress = spinner();
10
+ progress.start(`${stage.title} started`);
11
+ for (const probe of stage.getProbes(context)) {
12
+ if (probe.progressMessage) {
13
+ progress.message(probe.progressMessage);
14
+ }
15
+ const shouldRun = probe.runIf?.(context) ?? { run: true };
16
+ if (!shouldRun.run) {
17
+ context.results.push({
18
+ id: probe.id,
19
+ label: `${probe.label} - ${shouldRun.reason}`,
20
+ status: 'skip',
21
+ section: stage.id
22
+ });
23
+ continue;
24
+ }
25
+ if (!probe.run) {
26
+ throw new Error(`Probe "${probe.id}" is missing a run handler`);
27
+ }
28
+ try {
29
+ const output = await probe.run(context);
30
+ context.results.push({
31
+ id: probe.id,
32
+ label: probe.label,
33
+ status: output?.status ?? 'pass',
34
+ details: output?.details,
35
+ values: output?.values,
36
+ section: stage.id
37
+ });
38
+ }
39
+ catch (error) {
40
+ context.results.push({
41
+ id: probe.id,
42
+ label: probe.label,
43
+ status: 'fail',
44
+ details: [getThrownReason(error)],
45
+ section: stage.id
46
+ });
47
+ }
48
+ }
49
+ progress.stop(`${stage.title} completed`);
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+ export function passed(...args) {
3
+ const deps = typeof args[0] === 'string' ? [[args[0], args[1]]] : args;
4
+ return (ctx) => {
5
+ for (const [id, reason] of deps) {
6
+ if (!ctx.results.some((r) => r.id === id && r.status === 'pass'))
7
+ return {
8
+ run: false,
9
+ reason
10
+ };
11
+ }
12
+ return {
13
+ run: true
14
+ };
15
+ };
16
+ }
17
+ export function formatHexQuantity(value) {
18
+ if (!value.startsWith('0x')) {
19
+ return value;
20
+ }
21
+ return BigInt(value).toString(10);
22
+ }
23
+ export function shortenAddress(address) {
24
+ if (address.length <= 12) {
25
+ return address;
26
+ }
27
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
28
+ }
29
+ export function getRawReason(status, body) {
30
+ const trimmedBody = body.trim();
31
+ if (trimmedBody.length === 0) {
32
+ return `HTTP ${status}`;
33
+ }
34
+ return `HTTP ${status}: ${trimmedBody}`;
35
+ }
36
+ export function getThrownReason(error) {
37
+ return error instanceof Error ? error.message : String(error);
38
+ }
39
+ export function validateAndNormalizeUrls(prividiumRpcUrl, userPanelUrl) {
40
+ const apiParse = z.url().safeParse(prividiumRpcUrl);
41
+ if (!apiParse.success) {
42
+ throw new Error(`Invalid API url provided: ${prividiumRpcUrl}`);
43
+ }
44
+ const panelParse = z.url().safeParse(userPanelUrl);
45
+ if (!panelParse.success) {
46
+ throw new Error(`Invalid user panel url provided: ${userPanelUrl}`);
47
+ }
48
+ return {
49
+ apiBaseUrl: `${new URL(prividiumRpcUrl).origin}/rpc`,
50
+ userPanelBaseUrl: new URL(userPanelUrl).origin
51
+ };
52
+ }
@@ -0,0 +1,77 @@
1
+ import { intro } from '@clack/prompts';
2
+ import { authenticationStage } from './doctor/probes/authentication.js';
3
+ import { createBridgingStage } from './doctor/probes/bridging.js';
4
+ import { globalStage } from './doctor/probes/global.js';
5
+ import { createWalletStage } from './doctor/probes/wallet.js';
6
+ import { walletApiStage } from './doctor/probes/wallet-api.js';
7
+ import { buildDoctorReport } from './doctor/report/build.js';
8
+ import { printReport } from './doctor/report/render.js';
9
+ import { executeDoctorStages } from './doctor/stages.js';
10
+ import { showPrividiumHeader } from './utils/show-prividium-header';
11
+ import { gatherUrlConfig } from './utils/url-config.js';
12
+ async function runDoctor(opts) {
13
+ showPrividiumHeader();
14
+ intro('Prividium™ Doctor');
15
+ const { prividiumRpcUrl, userPanelUrl } = await gatherUrlConfig({
16
+ configPath: opts.configPath,
17
+ rpcUrl: opts.rpcUrl,
18
+ userPanelUrl: opts.userPanelUrl,
19
+ rpcUrlLabel: 'prividium api'
20
+ });
21
+ const executionContext = {
22
+ rawRpcUrl: prividiumRpcUrl,
23
+ rawUserPanelUrl: userPanelUrl,
24
+ walletAddresses: [],
25
+ walletApiAvailable: false,
26
+ results: []
27
+ };
28
+ const fixedStages = [globalStage, authenticationStage, walletApiStage];
29
+ await executeDoctorStages(executionContext, fixedStages);
30
+ const { walletAddresses } = executionContext;
31
+ const dynamicStages = [];
32
+ if (walletAddresses.length === 0) {
33
+ executionContext.results.push({
34
+ id: 'wallet',
35
+ label: `Wallet - ${[executionContext.authState ? 'No associated wallets' : 'Authentication not completed']}`,
36
+ status: 'skip',
37
+ section: 'Wallets'
38
+ });
39
+ }
40
+ else {
41
+ dynamicStages.push(...walletAddresses.flatMap((addr, i) => [createWalletStage(addr, i, walletAddresses.length)]));
42
+ dynamicStages.push(createBridgingStage(walletAddresses[0]));
43
+ await executeDoctorStages(executionContext, dynamicStages);
44
+ }
45
+ const report = buildDoctorReport(executionContext, [...fixedStages, ...dynamicStages]);
46
+ printReport(report);
47
+ if (report.status === 'fail') {
48
+ process.exitCode = 1;
49
+ }
50
+ }
51
+ export const addDoctor = (cli) => {
52
+ return cli.command('doctor', 'Runs Prividium Doctor checks', (yargs) => yargs
53
+ .option('rpcUrl', {
54
+ alias: ['prividium-api-url', 'rpc-url', 'r'],
55
+ description: 'Target Prividium™ API URL or /rpc URL. Takes precedence over config file and env.',
56
+ demandOption: false,
57
+ type: 'string'
58
+ })
59
+ .option('userPanelUrl', {
60
+ alias: ['user-panel-url', 'u'],
61
+ description: 'User panel URL for browser authentication. Takes precedence over config file and env.',
62
+ type: 'string'
63
+ }), async (args) => {
64
+ try {
65
+ await runDoctor({
66
+ rpcUrl: args.rpcUrl,
67
+ userPanelUrl: args.userPanelUrl,
68
+ configPath: args.configPath
69
+ });
70
+ }
71
+ catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ console.error(`Prividium doctor failed: ${message}`);
74
+ process.exit(1);
75
+ }
76
+ });
77
+ };
@@ -1,13 +1,8 @@
1
1
  import { log } from '@clack/prompts';
2
2
  import color from 'kleur';
3
- import { z } from 'zod';
4
- import { ConfigFile } from '../server/config-file.js';
5
3
  import { CreationWorkflow } from '../server/connection-workflow.js';
6
4
  import { buildServer } from '../server/server.js';
7
- const envSchema = z.object({
8
- PRIVIDIUM_RPC_URL: z.string().optional(),
9
- USER_PANEL_URL: z.string().optional()
10
- });
5
+ import { gatherUrlConfig } from './utils/url-config.js';
11
6
  const DEFAULT_PORT = 24101;
12
7
  function checkHostAndPortWarnings(host, port, allowExternalAccess) {
13
8
  if (port !== DEFAULT_PORT) {
@@ -27,14 +22,15 @@ function checkHostAndPortWarnings(host, port, allowExternalAccess) {
27
22
  }
28
23
  }
29
24
  async function startServer(opts) {
30
- const config = new ConfigFile(opts.configPath);
31
- const workflow = new CreationWorkflow(config, opts.host, opts.port);
25
+ const workflow = new CreationWorkflow(opts.host, opts.port);
32
26
  workflow.start();
33
- const configConfig = config.read();
34
- const env = envSchema.parse(process.env);
35
- const givenRpcUrl = opts.rpcUrl ?? env.PRIVIDIUM_RPC_URL ?? configConfig.prividiumRpcUrl;
36
- const givenUserPanelUrl = opts.userPanelUrl ?? env.USER_PANEL_URL ?? configConfig.userPanelUrl;
37
- const { prividiumRpcUrl, userPanelUrl } = await workflow.gatherData(givenRpcUrl, givenUserPanelUrl);
27
+ const { prividiumRpcUrl, userPanelUrl } = await gatherUrlConfig({
28
+ configPath: opts.configPath,
29
+ rpcUrl: opts.rpcUrl,
30
+ userPanelUrl: opts.userPanelUrl,
31
+ rpcUrlLabel: 'prividium rpc',
32
+ logProvidedUrls: true
33
+ });
38
34
  const app = buildServer({
39
35
  prividiumRpcUrl,
40
36
  userPanelUrl,
@@ -97,12 +93,21 @@ export const addProxy = (cli) => {
97
93
  description: 'Allow server to be exposed to the network (accessible by other devices)',
98
94
  default: false,
99
95
  type: 'boolean'
100
- }), (args) => startServer({
101
- rpcUrl: args.rpcUrl,
102
- userPanelUrl: args.userPanelUrl,
103
- configPath: args.configPath,
104
- port: args.port,
105
- host: args.host,
106
- allowExternalAccess: args.unsecureAllowOutsideAccess
107
- }));
96
+ }), async (args) => {
97
+ try {
98
+ await startServer({
99
+ rpcUrl: args.rpcUrl,
100
+ userPanelUrl: args.userPanelUrl,
101
+ configPath: args.configPath,
102
+ port: args.port,
103
+ host: args.host,
104
+ allowExternalAccess: args.unsecureAllowOutsideAccess
105
+ });
106
+ }
107
+ catch (error) {
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ console.error(`Prividium proxy failed: ${message}`);
110
+ process.exit(1);
111
+ }
112
+ });
108
113
  };
@@ -0,0 +1,11 @@
1
+ import color from 'kleur';
2
+ export const HEADER = ` .__ .__ .___.__ .__ .__
3
+ _____________|__|__ _|__| __| _/|__|__ __ _____ ____ | | |__|
4
+ \\____ \\_ __ \\ \\ \\/ / |/ __ | | | | \\/ \\ ______ _/ ___\\| | | |
5
+ | |_> > | \\/ |\\ /| / /_/ | | | | / Y Y \\ /_____/ \\ \\___| |_| |
6
+ | __/|__| |__| \\_/ |__\\____ | |__|____/|__|_| / \\___ >____/__|
7
+ |__| \\/ \\/ \\/ `;
8
+ export function showPrividiumHeader() {
9
+ console.log(color.blue(HEADER));
10
+ console.log('');
11
+ }
@@ -0,0 +1,50 @@
1
+ import { confirm, log, text } from '@clack/prompts';
2
+ import { z } from 'zod';
3
+ import { ConfigFile } from '../../server/config-file.js';
4
+ const envSchema = z.object({
5
+ PRIVIDIUM_RPC_URL: z.string().optional(),
6
+ USER_PANEL_URL: z.string().optional()
7
+ });
8
+ async function askForUrl(maybeUrl, name, logProvidedUrls) {
9
+ if (maybeUrl !== undefined) {
10
+ const res = z.url().safeParse(maybeUrl);
11
+ if (!res.success) {
12
+ throw new Error(`Invalid ${name} url provided: ${maybeUrl}`);
13
+ }
14
+ if (logProvidedUrls) {
15
+ log.info(`Using ${name} url: ${maybeUrl}`);
16
+ }
17
+ return { url: maybeUrl, prompted: false };
18
+ }
19
+ const url = await text({
20
+ message: `Please insert your ${name} url`,
21
+ validate(value) {
22
+ const parsed = z.url().safeParse(value);
23
+ if (!parsed.success) {
24
+ return 'Please provide a valid url';
25
+ }
26
+ }
27
+ });
28
+ if (typeof url === 'symbol') {
29
+ throw new Error('Canceled by the user');
30
+ }
31
+ return { url, prompted: true };
32
+ }
33
+ export async function gatherUrlConfig({ configPath, rpcUrl, userPanelUrl, rpcUrlLabel, logProvidedUrls = false }) {
34
+ const config = new ConfigFile(configPath);
35
+ const configData = config.read();
36
+ const env = envSchema.parse(process.env);
37
+ const givenRpcUrl = rpcUrl ?? env.PRIVIDIUM_RPC_URL ?? configData.prividiumRpcUrl;
38
+ const givenUserPanelUrl = userPanelUrl ?? env.USER_PANEL_URL ?? configData.userPanelUrl;
39
+ const { url: prividiumRpcUrl, prompted: promptedRpc } = await askForUrl(givenRpcUrl, rpcUrlLabel, logProvidedUrls);
40
+ const { url: resolvedUserPanelUrl, prompted: promptedPanel } = await askForUrl(givenUserPanelUrl, 'user panel', logProvidedUrls);
41
+ if (promptedRpc || promptedPanel) {
42
+ const confirmation = await confirm({
43
+ message: 'Do you want to store this config for future usages?'
44
+ });
45
+ if (confirmation) {
46
+ config.write({ prividiumRpcUrl, userPanelUrl: resolvedUserPanelUrl });
47
+ }
48
+ }
49
+ return { prividiumRpcUrl, userPanelUrl: resolvedUserPanelUrl };
50
+ }
package/dist/cli/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { BASE_CLI } from './base-cli.js';
2
2
  import { addConfig } from './commands/config.js';
3
+ import { addDoctor } from './commands/doctor.js';
3
4
  import { addProxy } from './commands/proxy.js';
4
- const actions = [addProxy, addConfig];
5
+ const actions = [addProxy, addDoctor, addConfig];
5
6
  const baseCli = actions.reduce((argv, applyAction) => {
6
7
  return applyAction(argv);
7
8
  }, BASE_CLI);
@@ -1,73 +1,21 @@
1
1
  import { setTimeout } from 'node:timers/promises';
2
- import { confirm, intro, log, tasks, text } from '@clack/prompts';
2
+ import { intro, log, tasks } from '@clack/prompts';
3
3
  import color from 'kleur';
4
4
  import open from 'open';
5
- import { z } from 'zod';
6
- const HEADER = ` .__ .__ .___.__ .__ .__
7
- _____________|__|__ _|__| __| _/|__|__ __ _____ ____ | | |__|
8
- \\____ \\_ __ \\ \\ \\/ / |/ __ | | | | \\/ \\ ______ _/ ___\\| | | |
9
- | |_> > | \\/ |\\ /| / /_/ | | | | / Y Y \\ /_____/ \\ \\___| |_| |
10
- | __/|__| |__| \\_/ |__\\____ | |__|____/|__|_| / \\___ >____/__|
11
- |__| \\/ \\/ \\/ `;
5
+ import { showPrividiumHeader } from '../commands/utils/show-prividium-header';
12
6
  export class CreationWorkflow {
13
- config;
14
7
  submitCallback;
15
8
  host;
16
9
  port;
17
- constructor(config, host, port) {
18
- this.config = config;
10
+ constructor(host, port) {
19
11
  this.submitCallback = undefined;
20
12
  this.host = host;
21
13
  this.port = port;
22
14
  }
23
15
  start() {
24
- console.log(color.blue(HEADER));
25
- console.log('');
16
+ showPrividiumHeader();
26
17
  intro('Starting Prividium™ proxy');
27
18
  }
28
- async gatherData(maybePrividiumRpcUrl, maybeUserPanelUrl) {
29
- const { url: prividiumRpcUrl, prompted: prompted1 } = await this.askForUrl(maybePrividiumRpcUrl, 'prividium rpc');
30
- const { url: userPanelUrl, prompted: prompted2 } = await this.askForUrl(maybeUserPanelUrl, 'user panel');
31
- if (prompted1 || prompted2) {
32
- const confirmation = await confirm({
33
- message: 'Do wou want to store this config for future usages?'
34
- });
35
- if (confirmation) {
36
- this.config.write({ prividiumRpcUrl, userPanelUrl });
37
- }
38
- }
39
- return {
40
- prividiumRpcUrl,
41
- userPanelUrl
42
- };
43
- }
44
- async askForUrl(maybeUrl, name) {
45
- if (maybeUrl !== undefined) {
46
- const res = z.url().safeParse(maybeUrl);
47
- if (!res.success) {
48
- log.error(`Invalid ${name} url provided: ${maybeUrl}`);
49
- process.exit(1);
50
- }
51
- log.info(`Using ${name} url: ${color.bold(maybeUrl)}`);
52
- return { url: maybeUrl, prompted: false };
53
- }
54
- else {
55
- const url = await text({
56
- message: `Please insert your ${name} url`,
57
- validate(value) {
58
- const parsed = z.url().safeParse(value);
59
- if (!parsed.success) {
60
- return 'Please provide a valid url';
61
- }
62
- }
63
- });
64
- if (typeof url === 'symbol') {
65
- log.warn('Canceled by the user');
66
- process.exit(1);
67
- }
68
- return { url, prompted: true };
69
- }
70
- }
71
19
  async waitForAuthentication(url) {
72
20
  log.info(`Please log in: ${url}`);
73
21
  // Attempt to open the browser automatically
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { extractResponseError } from './error-utils.js';
3
3
  const siweMessageResponseSchema = z.object({
4
- msg: z.string()
4
+ msg: z.string(),
5
+ nonceToken: z.string()
5
6
  });
6
7
  const siweLoginResponseSchema = z.object({
7
8
  token: z.string(),
@@ -39,7 +40,7 @@ export class SiweAuth {
39
40
  const loginResponse = await fetch(new URL('/api/auth/login/crypto-native', this.config.prividiumApiBaseUrl), {
40
41
  method: 'POST',
41
42
  headers: { 'Content-Type': 'application/json' },
42
- body: JSON.stringify({ message: siweData.msg, signature })
43
+ body: JSON.stringify({ message: siweData.msg, signature, nonceToken: siweData.nonceToken })
43
44
  });
44
45
  if (!loginResponse.ok) {
45
46
  const detail = await extractResponseError(loginResponse);
@@ -1 +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"}
1
+ {"root":["../cli/base-cli.ts","../cli/index.ts","../cli/commands/config.ts","../cli/commands/doctor.ts","../cli/commands/proxy.ts","../cli/commands/doctor/constants.ts","../cli/commands/doctor/profile.ts","../cli/commands/doctor/stages.ts","../cli/commands/doctor/types.ts","../cli/commands/doctor/utils.ts","../cli/commands/doctor/clients/browser-auth.ts","../cli/commands/doctor/clients/http.ts","../cli/commands/doctor/clients/rpc.ts","../cli/commands/doctor/clients/wallet-api.ts","../cli/commands/doctor/probes/authentication.ts","../cli/commands/doctor/probes/bridging.ts","../cli/commands/doctor/probes/global.ts","../cli/commands/doctor/probes/wallet-api.ts","../cli/commands/doctor/probes/wallet.ts","../cli/commands/doctor/probes/authentication/authentication.ts","../cli/commands/doctor/probes/authentication/wallet-preconditions.ts","../cli/commands/doctor/probes/bridging/bridging.ts","../cli/commands/doctor/probes/global/input-validation.ts","../cli/commands/doctor/probes/global/reachability.ts","../cli/commands/doctor/probes/wallet/authenticated-rpc.ts","../cli/commands/doctor/probes/wallet/authorization-and-wallet-rpc.ts","../cli/commands/doctor/report/build.ts","../cli/commands/doctor/report/render.ts","../cli/commands/utils/show-prividium-header.ts","../cli/commands/utils/url-config.ts","../cli/server/config-file.ts","../cli/server/connection-workflow.ts","../cli/server/server.ts"],"version":"5.8.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prividium",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "prividium": "bin/cli.js"