Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Tampered receipt:
- Production verification resolves signer keys from ENS TXT records: `cl.sig.pub`, `cl.sig.kid`, `cl.sig.canonical`, and `cl.receipt.signer`.
- Local key fallback is test/demo only and must be explicitly enabled with `COMMANDLAYER_ALLOW_LOCAL_KEY_FALLBACK=true` (or test mode).
- Verifier responses expose `public_key_source` as `ens_txt` or `local_test_fallback`.
- Production environment variable guidance (RPC, ENS ownership lookup, signing, and webhook/provider secrets): `docs/ops/environment.md`.

## Trust boundaries

Expand Down
32 changes: 29 additions & 3 deletions api/ens/owned.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,28 @@ function getAddressInput(req) {
}

function hasProviderConfig() {
return Boolean(process.env.ALCHEMY_ETH_RPC_URL || process.env.ETH_RPC_URL || process.env.ALCHEMY_ETH_API_KEY || process.env.SIMPLEHASH_API_KEY);
return Boolean(
getMainnetRpcUrl() ||
process.env.ALCHEMY_ETH_API_KEY ||
process.env.ALCHEMY_API_KEY ||
process.env.SIMPLEHASH_API_KEY
);
}

function getMainnetRpcUrl() {
const direct = [
process.env.ETHEREUM_RPC_URL,
process.env.MAINNET_RPC_URL,
process.env.ALCHEMY_ETHEREUM_RPC_URL,
process.env.ALCHEMY_ETH_RPC_URL,
process.env.ETH_RPC_URL,
].find((value) => typeof value === 'string' && value.trim());
if (direct) return direct.trim();

const alchemyKey = [process.env.ALCHEMY_API_KEY, process.env.ALCHEMY_ETH_API_KEY]
.find((value) => typeof value === 'string' && value.trim());
if (alchemyKey) return `https://eth-mainnet.g.alchemy.com/v2/${alchemyKey.trim()}`;
return '';
}

function normalizeAddress(raw) {
Expand All @@ -20,8 +41,9 @@ function normalizeAddress(raw) {
async function reverseResolvePrimary(address) {
try {
const { ethers } = require('ethers');
const rpcUrl = process.env.ALCHEMY_ETH_RPC_URL || process.env.ETH_RPC_URL || '';
const provider = rpcUrl ? new ethers.JsonRpcProvider(rpcUrl) : new ethers.AlchemyProvider('mainnet', process.env.ALCHEMY_ETH_API_KEY);
const rpcUrl = getMainnetRpcUrl();
if (!rpcUrl) return null;
const provider = new ethers.JsonRpcProvider(rpcUrl);
const primary = await provider.lookupAddress(address);
return primary || null;
} catch {
Expand Down Expand Up @@ -92,3 +114,7 @@ module.exports = async function handler(req, res) {
}))
});
};

module.exports._private = {
getMainnetRpcUrl,
};
34 changes: 34 additions & 0 deletions docs/ops/environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Environment configuration (operations)

This document lists production-focused environment variables used by `commandlayer-org` verifier, ENS lookup endpoints, and signing paths.

## Preferred production RPC configuration

For production ENS-related lookups, configure **one explicit mainnet RPC endpoint**:

- Preferred: `ETHEREUM_RPC_URL`
- Backward-compatible alternatives: `MAINNET_RPC_URL`, `ALCHEMY_ETHEREUM_RPC_URL`
- If only `ALCHEMY_API_KEY` is set, the app constructs `https://eth-mainnet.g.alchemy.com/v2/<key>`.
- Default/provider fallback is last resort and can hit shared-rate limits.

## Environment variable table

| Env var | Required | Used by | Production purpose | Safe example placeholder |
|---|---|---|---|---|
| `ETHEREUM_RPC_URL` | Recommended | `api/ens/owned.js` | Primary explicit Ethereum mainnet RPC for ENS reverse lookup without shared default provider throttling. | `https://mainnet.example-rpc.com/v1/<project-id>` |
| `MAINNET_RPC_URL` | Optional | `api/ens/owned.js` | Backward-compatible alternative mainnet RPC variable. | `https://mainnet.example-rpc.com/v1/<project-id>` |
| `ALCHEMY_ETHEREUM_RPC_URL` | Optional | `api/ens/owned.js` | Backward-compatible explicit Alchemy HTTPS RPC URL. | `https://eth-mainnet.g.alchemy.com/v2/<alchemy-key>` |
| `ALCHEMY_API_KEY` | Optional | `api/ens/owned.js` | If set (without explicit RPC URL), converted to an Alchemy mainnet HTTPS RPC URL. | `<alchemy-api-key>` |
| `SIMPLEHASH_API_KEY` | Optional | `api/ens/owned.js` | API key for ENS ownership lookup via SimpleHash NFT owner API (separate from ENS TXT verification). | `<simplehash-api-key>` |
| `COMMANDLAYER_ALLOW_LOCAL_KEY_FALLBACK` | Optional (dangerous in prod) | `lib/verifyReceipt.js` | Enables local test/demo signer-key fallback. **Do not enable in production** unless intentionally running demo fallback behavior. | `false` |
| `COINBASE_WEBHOOK_SECRET` | Required (if Coinbase webhook endpoint enabled) | `api/examples/coinbase-webhook.js`, `lib/coinbaseWebhook.js` | Verifies Coinbase webhook signatures. | `<coinbase-webhook-secret>` |
| `CL_RECEIPT_SIGNER_ID` | Required (for CL-prefixed signing path) | `lib/receiptSigning.js`, webhook/x402 APIs | Canonical signer ID used in receipt proof metadata. | `runtime.commandlayer.eth` |
| `CL_RECEIPT_SIGNING_KID` | Required (for CL-prefixed signing path) | `lib/receiptSigning.js`, webhook/x402 APIs | Key identifier for receipt signature metadata. | `kid_prod_2026_01` |
| `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` | Required (legacy/non-CL path) | `lib/receiptSigning.js`, webhook/x402 APIs | Base64-encoded PEM private key used to sign receipts when legacy variable path is used. | `<base64-encoded-pem>` |
| `X402_PROVIDER_VERIFICATION_URL` | Optional (required for provider-verification mode) | `lib/x402ProviderVerification.js`, `api/examples/x402-paid-action.js` | External payment-provider verification endpoint for x402 flow. | `https://provider.example.com/verify` |
| `X402_PROVIDER_API_KEY` | Optional | `lib/x402ProviderVerification.js` | Bearer token for provider-verification endpoint authentication. | `<provider-api-key>` |

## Notes

- ENS TXT verification for receipt keys in `lib/verifyReceipt.js` is resolver-injected and ENS-first; local fallback remains gated.
- `api/ens/owned.js` ownership discovery uses SimpleHash and reverse lookup RPC/provider logic, which is separate from receipt proof verification.
46 changes: 46 additions & 0 deletions tests/api-ens-owned.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ test('GET /api/ens/owned rejects invalid address', async () => {

test('GET /api/ens/owned returns provider unavailable when config missing', async () => {
const prev = { ...process.env };
delete process.env.ETHEREUM_RPC_URL;
delete process.env.MAINNET_RPC_URL;
delete process.env.ALCHEMY_ETHEREUM_RPC_URL;
delete process.env.ALCHEMY_API_KEY;
delete process.env.ALCHEMY_ETH_API_KEY;
delete process.env.ALCHEMY_ETH_RPC_URL;
delete process.env.ETH_RPC_URL;
Expand All @@ -45,6 +49,10 @@ test('GET /api/ens/owned returns provider unavailable when config missing', asyn

test('GET /api/ens/owned stable unavailable response shape', async () => {
const prev = { ...process.env };
delete process.env.ETHEREUM_RPC_URL;
delete process.env.MAINNET_RPC_URL;
delete process.env.ALCHEMY_ETHEREUM_RPC_URL;
delete process.env.ALCHEMY_API_KEY;
delete process.env.ALCHEMY_ETH_API_KEY;
delete process.env.ALCHEMY_ETH_RPC_URL;
delete process.env.ETH_RPC_URL;
Expand Down Expand Up @@ -99,3 +107,41 @@ test('GET /api/ens/owned returns ENS names when SimpleHash key is configured', a
process.env = prevEnv;
global.fetch = prevFetch;
});

test('mainnet RPC resolution prefers ETHEREUM_RPC_URL', async () => {
const prevEnv = { ...process.env };
const { _private } = require('../api/ens/owned');
process.env.ETHEREUM_RPC_URL = 'https://rpc-1.example';
process.env.MAINNET_RPC_URL = 'https://rpc-2.example';
process.env.ALCHEMY_ETHEREUM_RPC_URL = 'https://rpc-3.example';
process.env.ALCHEMY_API_KEY = 'alchemy';
assert.equal(_private.getMainnetRpcUrl(), 'https://rpc-1.example');
process.env = prevEnv;
});

test('mainnet RPC resolution maps ALCHEMY_API_KEY to alchemy HTTPS URL', async () => {
const prevEnv = { ...process.env };
const { _private } = require('../api/ens/owned');
delete process.env.ETHEREUM_RPC_URL;
delete process.env.MAINNET_RPC_URL;
delete process.env.ALCHEMY_ETHEREUM_RPC_URL;
delete process.env.ALCHEMY_ETH_RPC_URL;
delete process.env.ETH_RPC_URL;
process.env.ALCHEMY_API_KEY = 'abc123';
assert.equal(_private.getMainnetRpcUrl(), 'https://eth-mainnet.g.alchemy.com/v2/abc123');
process.env = prevEnv;
});

test('mainnet RPC resolution returns empty string as last resort', async () => {
const prevEnv = { ...process.env };
const { _private } = require('../api/ens/owned');
delete process.env.ETHEREUM_RPC_URL;
delete process.env.MAINNET_RPC_URL;
delete process.env.ALCHEMY_ETHEREUM_RPC_URL;
delete process.env.ALCHEMY_ETH_RPC_URL;
delete process.env.ETH_RPC_URL;
delete process.env.ALCHEMY_API_KEY;
delete process.env.ALCHEMY_ETH_API_KEY;
assert.equal(_private.getMainnetRpcUrl(), '');
process.env = prevEnv;
});
Loading