Skip to content

Commit cba2ba4

Browse files
committed
feat: wire decentralized-first RPC endpoints with POKT as primary default (ev-iqh40)
POKT Network (Nodies) endpoints are now the first-choice RPC provider for Ethereum, Polygon, Arbitrum, and Optimism. Adds @cygnus-wealth/rpc-infrastructure types for canonical endpoint config, build-time bundle assertion to prevent leaked API keys, and 52 new tests covering endpoint hierarchy and secret scanning.
1 parent 9c9f90d commit cba2ba4

15 files changed

Lines changed: 831 additions & 2 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"verify:websocket": "tsx src/e2e/websocketVerification.ts",
2222
"demo": "cd demo-ui && npm install && npm run dev",
2323
"dev:ui": "cd test-ui && npm install && npm run dev",
24+
"build:check": "tsup && tsx scripts/check-bundle-secrets.ts",
2425
"prepublishOnly": "npm run build"
2526
},
2627
"keywords": [

scripts/check-bundle-secrets.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env tsx
2+
/**
3+
* Build-time assertion: scans the production bundle for leaked API keys,
4+
* env var values, and managed provider URLs.
5+
*
6+
* Run: npx tsx scripts/check-bundle-secrets.ts
7+
*/
8+
import { readFileSync } from 'fs';
9+
import { assertNoBundledSecrets } from '../src/rpc/build-assertions.js';
10+
11+
const BUNDLE_PATH = './dist/index.js';
12+
13+
try {
14+
const content = readFileSync(BUNDLE_PATH, 'utf-8');
15+
assertNoBundledSecrets(content);
16+
console.log('Build assertion PASSED: no bundled secrets detected in', BUNDLE_PATH);
17+
} catch (err) {
18+
console.error('Build assertion FAILED:', (err as Error).message);
19+
process.exit(1);
20+
}

src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ export {
136136
RpcProviderType,
137137
DEFAULT_HEALTH_CHECK_INTERVAL_MS,
138138
METRICS_ROLLING_WINDOW_MS,
139+
// Decentralized-first defaults
140+
getDecentralizedRpcConfig,
141+
getChainRpcConfig,
142+
assertNoBundledApiKeys,
143+
SUPPORTED_CHAIN_IDS,
144+
POKT_ENDPOINTS,
145+
API_KEY_PATTERNS,
146+
// Build-time assertions
147+
assertNoBundledSecrets,
139148
} from './rpc/index.js';
140149

141150
export type {

src/registry/configs/arbitrum.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"explorer": "https://arbiscan.io",
77
"endpoints": {
88
"http": [
9+
"https://arb-pokt.nodies.app",
910
"https://arb1.arbitrum.io/rpc",
1011
"https://arbitrum-one-rpc.publicnode.com",
1112
"https://rpc.ankr.com/arbitrum"

src/registry/configs/ethereum.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"explorer": "https://etherscan.io",
77
"endpoints": {
88
"http": [
9+
"https://eth-pokt.nodies.app",
910
"https://ethereum-rpc.publicnode.com",
1011
"https://eth.llamarpc.com",
1112
"https://rpc.ankr.com/eth",

src/registry/configs/optimism.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"explorer": "https://optimistic.etherscan.io",
77
"endpoints": {
88
"http": [
9+
"https://op-pokt.nodies.app",
910
"https://mainnet.optimism.io",
1011
"https://optimism-rpc.publicnode.com",
1112
"https://rpc.ankr.com/optimism"

src/registry/configs/polygon.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"explorer": "https://polygonscan.com",
77
"endpoints": {
88
"http": [
9+
"https://polygon-pokt.nodies.app",
910
"https://polygon-rpc.com",
1011
"https://polygon-bor-rpc.publicnode.com",
1112
"https://rpc.ankr.com/polygon",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, it, expect } from 'vitest';
2+
import ethereumConfig from './configs/ethereum.json';
3+
import polygonConfig from './configs/polygon.json';
4+
import arbitrumConfig from './configs/arbitrum.json';
5+
import optimismConfig from './configs/optimism.json';
6+
import baseConfig from './configs/base.json';
7+
8+
describe('chain configs: decentralized-first endpoints', () => {
9+
const configs = [
10+
{ name: 'Ethereum', config: ethereumConfig, poktDomain: 'eth-pokt.nodies.app' },
11+
{ name: 'Polygon', config: polygonConfig, poktDomain: 'polygon-pokt.nodies.app' },
12+
{ name: 'Arbitrum', config: arbitrumConfig, poktDomain: 'arb-pokt.nodies.app' },
13+
{ name: 'Optimism', config: optimismConfig, poktDomain: 'op-pokt.nodies.app' },
14+
];
15+
16+
for (const { name, config, poktDomain } of configs) {
17+
describe(name, () => {
18+
it('should have POKT (Nodies) as the first HTTP endpoint', () => {
19+
expect(config.endpoints.http[0]).toContain(poktDomain);
20+
});
21+
22+
it('should not have Alchemy or Infura endpoints', () => {
23+
for (const url of config.endpoints.http) {
24+
expect(url).not.toContain('alchemy');
25+
expect(url).not.toContain('infura');
26+
}
27+
});
28+
29+
it('should not contain API keys in any endpoint URL', () => {
30+
for (const url of config.endpoints.http) {
31+
// No /v2/<key> or /v3/<key> patterns
32+
expect(url).not.toMatch(/\/v[23]\/[a-zA-Z0-9]{20,}/);
33+
}
34+
if (config.endpoints.ws) {
35+
for (const url of config.endpoints.ws) {
36+
expect(url).not.toMatch(/\/v[23]\/[a-zA-Z0-9]{20,}/);
37+
}
38+
}
39+
});
40+
41+
it('should have at least 3 HTTP fallback endpoints', () => {
42+
expect(config.endpoints.http.length).toBeGreaterThanOrEqual(3);
43+
});
44+
});
45+
}
46+
47+
describe('Base (no POKT available)', () => {
48+
it('should have public endpoints as primary', () => {
49+
expect(baseConfig.endpoints.http.length).toBeGreaterThanOrEqual(3);
50+
});
51+
52+
it('should not have Alchemy or Infura endpoints', () => {
53+
for (const url of baseConfig.endpoints.http) {
54+
expect(url).not.toContain('alchemy');
55+
expect(url).not.toContain('infura');
56+
}
57+
});
58+
});
59+
});

src/rpc/build-assertions.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { assertNoBundledSecrets } from './build-assertions.js';
3+
4+
describe('build-assertions', () => {
5+
describe('assertNoBundledSecrets', () => {
6+
it('should pass for clean content', () => {
7+
const clean = `
8+
const url = "https://eth-pokt.nodies.app";
9+
const rpc = "https://polygon-rpc.com";
10+
`;
11+
expect(() => assertNoBundledSecrets(clean)).not.toThrow();
12+
});
13+
14+
it('should fail if CYGNUS_RPC_ env var value is embedded', () => {
15+
const dirty = `const url = "CYGNUS_RPC_ETHEREUM_KEY_abc123";`;
16+
expect(() => assertNoBundledSecrets(dirty)).toThrow(/CYGNUS_RPC_/);
17+
});
18+
19+
it('should fail if Alchemy API key pattern is found', () => {
20+
const dirty = `const url = "https://eth-mainnet.g.alchemy.com/v2/a1b2c3d4e5f6g7h8i9j0";`;
21+
expect(() => assertNoBundledSecrets(dirty)).toThrow(/alchemy/i);
22+
});
23+
24+
it('should fail if Infura project ID pattern is found', () => {
25+
const dirty = `const url = "https://mainnet.infura.io/v3/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6";`;
26+
expect(() => assertNoBundledSecrets(dirty)).toThrow(/infura/i);
27+
});
28+
29+
it('should fail if generic long hex key pattern is found after /v2/ or /v3/', () => {
30+
const dirty = `const url = "https://rpc.example.com/v2/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8";`;
31+
expect(() => assertNoBundledSecrets(dirty)).toThrow(/API key/i);
32+
});
33+
34+
it('should not false-positive on known public endpoints', () => {
35+
const safe = `
36+
"https://eth-pokt.nodies.app"
37+
"https://polygon-rpc.com"
38+
"https://arb1.arbitrum.io/rpc"
39+
"https://ethereum-rpc.publicnode.com"
40+
"https://eth.llamarpc.com"
41+
"https://rpc.ankr.com/eth"
42+
"https://cloudflare-eth.com"
43+
"wss://eth-mainnet.public.blastapi.io"
44+
"https://ethereum-sepolia.blockpi.network/v1/rpc/public"
45+
`;
46+
expect(() => assertNoBundledSecrets(safe)).not.toThrow();
47+
});
48+
49+
it('should detect multiple violations and report all', () => {
50+
const dirty = `
51+
const a = "https://eth-mainnet.g.alchemy.com/v2/abc123";
52+
const b = "CYGNUS_RPC_KEY_xyz";
53+
`;
54+
expect(() => assertNoBundledSecrets(dirty)).toThrow();
55+
});
56+
});
57+
});

src/rpc/build-assertions.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Build-time assertions for production bundle safety.
3+
*
4+
* Scans bundle content for leaked API keys, env var values, and
5+
* managed provider URLs that should never appear in a client-side bundle.
6+
*
7+
* Usage:
8+
* import { assertNoBundledSecrets } from './build-assertions.js';
9+
* assertNoBundledSecrets(bundleContent);
10+
*
11+
* @module rpc/build-assertions
12+
*/
13+
14+
/** Patterns that should never appear in a production bundle */
15+
const BUNDLE_SECRET_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
16+
{ pattern: /CYGNUS_RPC_[A-Z_]+/g, label: 'CYGNUS_RPC_ env var reference' },
17+
{ pattern: /alchemy\.com\/v2\/[a-zA-Z0-9_-]{10,}/g, label: 'Alchemy API key in URL' },
18+
{ pattern: /alchemyapi\.io\/v2\/[a-zA-Z0-9_-]{10,}/g, label: 'Alchemy API key in URL' },
19+
{ pattern: /infura\.io\/v3\/[a-f0-9]{20,}/g, label: 'Infura project ID in URL' },
20+
{ pattern: /\/v[23]\/[a-zA-Z0-9_-]{32,}/g, label: 'Possible API key in /v2/ or /v3/ path' },
21+
];
22+
23+
/**
24+
* Assert that the given content (e.g., a production JS bundle) does not
25+
* contain any API key patterns or env var values.
26+
*
27+
* @param content - The string content to scan (typically a bundled JS file)
28+
* @throws Error if any secret patterns are found
29+
*/
30+
export function assertNoBundledSecrets(content: string): void {
31+
const violations: string[] = [];
32+
33+
for (const { pattern, label } of BUNDLE_SECRET_PATTERNS) {
34+
// Reset lastIndex for global patterns
35+
pattern.lastIndex = 0;
36+
const matches = content.match(pattern);
37+
if (matches) {
38+
for (const match of matches) {
39+
violations.push(`${label}: "${match}"`);
40+
}
41+
}
42+
}
43+
44+
if (violations.length > 0) {
45+
throw new Error(
46+
`Production bundle contains secret patterns — build REJECTED:\n` +
47+
violations.map(v => ` - ${v}`).join('\n')
48+
);
49+
}
50+
}

0 commit comments

Comments
 (0)