Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions packages/agents/analysis/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ import { stellar } from '@stellar/mpp/charge/server';
import { USDC_SAC_TESTNET, STELLAR_TESTNET } from '@stellar/mpp';
import { analyzeWithClaude } from './analyze.js';
import { registerSelf } from './register.js';
import { validateEnvOrExit } from '@clevercon/common';

if (!process.env.VITEST) {
validateEnvOrExit('analysis', {
REGISTRY_URL: { type: 'url' },
ANALYSIS_AGENT_SECRET_KEY: { type: 'stellarSecret' },
});
}

const PORT = parseInt(process.env.ANALYSIS_AGENT_PORT || process.env.PORT || '4004');
const SECRET_KEY = process.env.ANALYSIS_AGENT_SECRET_KEY!;

if (!SECRET_KEY) {
console.error('[AnalysisBot] ANALYSIS_AGENT_SECRET_KEY not set');
process.exit(1);
}

const keypair = Keypair.fromSecret(SECRET_KEY);
const PAY_TO = keypair.publicKey();

Expand Down
13 changes: 8 additions & 5 deletions packages/agents/reporter/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import { HTTPFacilitatorClient } from '@x402/core/server';
import { ExactStellarScheme } from '@x402/stellar/exact/server';
import { generateReport } from './report.js';
import { registerSelf } from './register.js';
import { validateEnvOrExit } from '@clevercon/common';

if (!process.env.VITEST) {
validateEnvOrExit('reporter', {
REGISTRY_URL: { type: 'url' },
REPORT_AGENT_SECRET_KEY: { type: 'stellarSecret' },
});
}

const PORT = parseInt(process.env.REPORT_AGENT_PORT || process.env.PORT || '4005');
const SECRET_KEY = process.env.REPORT_AGENT_SECRET_KEY!;
const FACILITATOR_URL = process.env.X402_FACILITATOR_URL || 'https://www.x402.org/facilitator';
const NETWORK = (process.env.STELLAR_NETWORK || 'stellar:testnet') as `${string}:${string}`;

if (!SECRET_KEY) {
console.error('[ReporterBot] REPORT_AGENT_SECRET_KEY not set');
process.exit(1);
}

const keypair = Keypair.fromSecret(SECRET_KEY);
const PAY_TO = keypair.publicKey();

Expand Down
13 changes: 8 additions & 5 deletions packages/agents/stellar-oracle/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ import {
import { getCacheStats } from './cache.js';
import { getCryptoQuote, getCryptoCandles } from './x402-consumer.js';
import { registerSelf } from './register.js';
import { validateEnvOrExit } from '@clevercon/common';

if (!process.env.VITEST) {
validateEnvOrExit('stellar-oracle', {
REGISTRY_URL: { type: 'url' },
STELLAR_ORACLE_SECRET_KEY: { type: 'stellarSecret' },
});
}

const PORT = parseInt(process.env.STELLAR_ORACLE_PORT || process.env.PORT || '4001');
const SECRET_KEY = process.env.STELLAR_ORACLE_SECRET_KEY!;
const FACILITATOR_URL = process.env.X402_FACILITATOR_URL || 'https://www.x402.org/facilitator';
const NETWORK = (process.env.STELLAR_NETWORK || 'stellar:testnet') as `${string}:${string}`;

if (!SECRET_KEY) {
console.error('[StellarOracle] STELLAR_ORACLE_SECRET_KEY not set');
process.exit(1);
}

const keypair = Keypair.fromSecret(SECRET_KEY);
const PAY_TO = keypair.publicKey();

Expand Down
13 changes: 8 additions & 5 deletions packages/agents/web-intel-v2/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import { HTTPFacilitatorClient } from '@x402/core/server';
import { ExactStellarScheme } from '@x402/stellar/exact/server';
import { getBlockchainNews } from './news.js';
import { registerSelf } from './register.js';
import { validateEnvOrExit } from '@clevercon/common';

if (!process.env.VITEST) {
validateEnvOrExit('web-intel-v2', {
REGISTRY_URL: { type: 'url' },
WEB_INTEL_V2_SECRET_KEY: { type: 'stellarSecret' },
});
}

const PORT = parseInt(process.env.WEB_INTEL_V2_PORT || process.env.PORT || '4003');
const SECRET_KEY = process.env.WEB_INTEL_V2_SECRET_KEY!;
const FACILITATOR_URL = process.env.X402_FACILITATOR_URL || 'https://www.x402.org/facilitator';
const NETWORK = (process.env.STELLAR_NETWORK || 'stellar:testnet') as `${string}:${string}`;

if (!SECRET_KEY) {
console.error('[WebIntelligenceV2] WEB_INTEL_V2_SECRET_KEY not set');
process.exit(1);
}

const keypair = Keypair.fromSecret(SECRET_KEY);
const PAY_TO = keypair.publicKey();

Expand Down
14 changes: 9 additions & 5 deletions packages/agents/web-intel/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ import { ExactStellarScheme } from '@x402/stellar/exact/server';
import { getBlockchainNews, getTechNews, getAINews } from './news.js';
import { scrapeUrl } from './scraper.js';
import { registerSelf } from './register.js';
import { validateEnvOrExit } from '@clevercon/common';

if (!process.env.VITEST) {
validateEnvOrExit('web-intel', {
REGISTRY_URL: { type: 'url' },
WEB_INTEL_SECRET_KEY: { type: 'stellarSecret' },
ANTHROPIC_API_KEY: { optional: true, description: 'only used for optional Claude summarisation' },
});
}

const PORT = parseInt(process.env.WEB_INTEL_PORT || process.env.PORT || '4002');
const SECRET_KEY = process.env.WEB_INTEL_SECRET_KEY!;
const FACILITATOR_URL = process.env.X402_FACILITATOR_URL || 'https://www.x402.org/facilitator';
const NETWORK = (process.env.STELLAR_NETWORK || 'stellar:testnet') as `${string}:${string}`;
const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY;

if (!SECRET_KEY) {
console.error('[WebIntelligence] WEB_INTEL_SECRET_KEY not set');
process.exit(1);
}

const keypair = Keypair.fromSecret(SECRET_KEY);
const PAY_TO = keypair.publicKey();

Expand Down
167 changes: 167 additions & 0 deletions packages/common/src/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { describe, it, expect } from 'vitest';
import { requireEnv, EnvValidationError } from './env.js';

// A valid 56-char Stellar secret key (S + 55 base32 chars A-Z2-7) used throughout tests.
const VALID_SECRET = 'SCJ4HXIMNDYRBCXE2QHKXCNHMFZKIOZBHGP4M2U7ZUPHD35OSEKTZKXV';
// A valid 56-char Stellar contract ID (C + 55 base32 chars).
const VALID_CONTRACT = 'CDFLEJ2HFPK3WKFTWB4CKP2JHEYNAUWKXGEJRYW4YMMGDSQSQ7D4LRTE';

describe('requireEnv', () => {
// ── Missing required key ────────────────────────────────────────────────────

it('throws EnvValidationError naming the key when a single required key is missing', () => {
const source = {};
expect(() => requireEnv({ MY_VAR: {} }, source)).toThrow(EnvValidationError);
expect(() => requireEnv({ MY_VAR: {} }, source)).toThrow('MY_VAR');
});

// ── Valid shapes return trimmed values ──────────────────────────────────────

it('accepts a present string value and returns the trimmed result', () => {
const source = { MY_STRING: ' hello world ' };
const result = requireEnv({ MY_STRING: { type: 'string' } }, source);
expect(result.MY_STRING).toBe('hello world');
});

it('accepts a valid URL and returns the trimmed value', () => {
const source = { MY_URL: ' https://example.com ' };
const result = requireEnv({ MY_URL: { type: 'url' } }, source);
expect(result.MY_URL).toBe('https://example.com');
});

it('accepts a valid number string and returns the trimmed value', () => {
const source = { MY_NUM: ' 42 ' };
const result = requireEnv({ MY_NUM: { type: 'number' } }, source);
expect(result.MY_NUM).toBe('42');
});

it('accepts a valid Stellar secret key', () => {
const source = { MY_SECRET: ` ${VALID_SECRET} ` };
const result = requireEnv({ MY_SECRET: { type: 'stellarSecret' } }, source);
expect(result.MY_SECRET).toBe(VALID_SECRET);
});

it('accepts a valid Stellar contract ID', () => {
const source = { MY_CONTRACT: ` ${VALID_CONTRACT} ` };
const result = requireEnv({ MY_CONTRACT: { type: 'stellarContract' } }, source);
expect(result.MY_CONTRACT).toBe(VALID_CONTRACT);
});

// ── Whitespace-only treated as missing ──────────────────────────────────────

it('treats a whitespace-only value as missing and throws', () => {
const source = { MY_VAR: ' ' };
const err = (() => {
try {
requireEnv({ MY_VAR: { type: 'string' } }, source);
} catch (e) {
return e;
}
})();
expect(err).toBeInstanceOf(EnvValidationError);
expect((err as EnvValidationError).issues[0]).toMatch(/MY_VAR/);
});

// ── All issues collected in one throw ───────────────────────────────────────

it('collects all three missing keys into a single error with three issues', () => {
const source = {};
let thrown: unknown;
try {
requireEnv({ A: {}, B: {}, C: {} }, source);
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(EnvValidationError);
const err = thrown as EnvValidationError;
expect(err.issues).toHaveLength(3);
expect(err.issues.some((i) => i.includes('A'))).toBe(true);
expect(err.issues.some((i) => i.includes('B'))).toBe(true);
expect(err.issues.some((i) => i.includes('C'))).toBe(true);
});

// ── Optional keys ───────────────────────────────────────────────────────────

it('does not throw when an optional key is missing', () => {
const source = {};
expect(() => requireEnv({ MY_OPT: { optional: true } }, source)).not.toThrow();
});

// ── LLM_PROVIDER=mock exception pattern ────────────────────────────────────

it('passes validation when ANTHROPIC_API_KEY is optional because LLM_PROVIDER=mock', () => {
const source = { LLM_PROVIDER: 'mock' };
// Simulate the spec built at startup: optional iff LLM_PROVIDER === 'mock'
const spec = {
ANTHROPIC_API_KEY: { optional: source.LLM_PROVIDER === 'mock' },
};
expect(() => requireEnv(spec, source)).not.toThrow();
});

it('fails validation when LLM_PROVIDER is not mock and ANTHROPIC_API_KEY is absent', () => {
const source = { LLM_PROVIDER: 'anthropic' };
const spec = {
ANTHROPIC_API_KEY: { optional: source.LLM_PROVIDER === 'mock' },
};
expect(() => requireEnv(spec, source)).toThrow(EnvValidationError);
expect(() => requireEnv(spec, source)).toThrow('ANTHROPIC_API_KEY');
});

it('fails validation when LLM_PROVIDER is unset and ANTHROPIC_API_KEY is absent', () => {
const source = {};
const spec = {
ANTHROPIC_API_KEY: { optional: source['LLM_PROVIDER' as keyof typeof source] === 'mock' },
};
expect(() => requireEnv(spec, source)).toThrow(EnvValidationError);
});

// ── Invalid shape values ────────────────────────────────────────────────────

it('throws naming the key when a URL value is malformed', () => {
const source = { MY_URL: 'not-a-url' };
let thrown: unknown;
try {
requireEnv({ MY_URL: { type: 'url' } }, source);
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(EnvValidationError);
expect((thrown as EnvValidationError).issues[0]).toMatch(/MY_URL/);
});

it('throws naming the key when a number value is non-numeric', () => {
const source = { MY_NUM: 'abc' };
let thrown: unknown;
try {
requireEnv({ MY_NUM: { type: 'number' } }, source);
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(EnvValidationError);
expect((thrown as EnvValidationError).issues[0]).toMatch(/MY_NUM/);
});

it('throws naming the key when a stellarSecret value is malformed', () => {
const source = { MY_SECRET: 'not-a-stellar-secret' };
let thrown: unknown;
try {
requireEnv({ MY_SECRET: { type: 'stellarSecret' } }, source);
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(EnvValidationError);
expect((thrown as EnvValidationError).issues[0]).toMatch(/MY_SECRET/);
});

it('throws naming the key when a stellarContract value is malformed', () => {
const source = { MY_CONTRACT: 'not-a-stellar-contract' };
let thrown: unknown;
try {
requireEnv({ MY_CONTRACT: { type: 'stellarContract' } }, source);
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(EnvValidationError);
expect((thrown as EnvValidationError).issues[0]).toMatch(/MY_CONTRACT/);
});
});
Loading
Loading