Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m3s
CI/CD Pipeline / Security Scanning (push) Successful in 2m58s
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 26s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 29s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 41s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 22s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 24s
Validation / validate-genesis (push) Successful in 32s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m31s
Validation / validate-documentation (push) Failing after 23s
Verify Deployment / Verify Deployment (push) Failing after 1m4s
Add BTC L1 settle/reconcile/ledger APIs, bitcoind intake, cBTC PMM hot-LP scripts, and custody credential smoke tests (secrets stay gitignored). Enables full-prod local green health and server pull-deploy for secure.omdnl.org /btc/*. Co-authored-by: Cursor <cursoragent@cursor.com>
235 lines
7.7 KiB
JavaScript
235 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Smoke-test Dfns + Cobo credentials from secrets/*.
|
|
* Usage: node scripts/custody/smoke-dfns-cobo.mjs
|
|
* Does not print secrets — only HTTP status and non-sensitive fields.
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import crypto from 'crypto';
|
|
import { fileURLToPath } from 'url';
|
|
import { createRequire } from 'module';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const root = path.resolve(__dirname, '../..');
|
|
|
|
function loadEnvFile(rel) {
|
|
const p = path.join(root, rel);
|
|
if (!fs.existsSync(p)) throw new Error(`Missing ${rel}`);
|
|
const out = {};
|
|
for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
|
|
if (!line || line.trim().startsWith('#')) continue;
|
|
const i = line.indexOf('=');
|
|
if (i < 0) continue;
|
|
out[line.slice(0, i).trim()] = line.slice(i + 1).trim();
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function redact(s) {
|
|
if (!s) return '(empty)';
|
|
if (s.length <= 12) return '***';
|
|
return `${s.slice(0, 6)}…${s.slice(-4)} (len=${s.length})`;
|
|
}
|
|
|
|
async function testDfns(dfns) {
|
|
const base = (dfns.DFNS_API_BASE_URL || 'https://api.dfns.io').replace(/\/$/, '');
|
|
const token = dfns.DFNS_AUTH_TOKEN;
|
|
if (!token) throw new Error('DFNS_AUTH_TOKEN missing');
|
|
|
|
const pemPath = path.isAbsolute(dfns.DFNS_PRIVATE_KEY_PATH)
|
|
? dfns.DFNS_PRIVATE_KEY_PATH
|
|
: path.join(root, dfns.DFNS_PRIVATE_KEY_PATH || 'secrets/dfns/rsa2048.pem');
|
|
const pemOk = fs.existsSync(pemPath);
|
|
const pem = pemOk ? fs.readFileSync(pemPath, 'utf8') : '';
|
|
const pemLooksPrivate = pem.includes('BEGIN PRIVATE KEY') || pem.includes('BEGIN RSA PRIVATE KEY');
|
|
|
|
// Read-only: list wallets (Bearer only)
|
|
const res = await fetch(`${base}/wallets?limit=5`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
const text = await res.text();
|
|
let body;
|
|
try {
|
|
body = JSON.parse(text);
|
|
} catch {
|
|
body = { raw: text.slice(0, 200) };
|
|
}
|
|
|
|
const items = body?.items ?? body?.wallets ?? [];
|
|
return {
|
|
ok: res.status >= 200 && res.status < 300,
|
|
http: res.status,
|
|
orgId: dfns.DFNS_ORG_ID || body?.items?.[0]?.orgId,
|
|
credIdPresent: Boolean(dfns.DFNS_CRED_ID),
|
|
credIdPrefix: redact(dfns.DFNS_CRED_ID),
|
|
tokenPrefix: redact(token),
|
|
privateKeyPem: pemOk && pemLooksPrivate ? 'present' : 'MISSING_OR_INVALID',
|
|
walletCount: Array.isArray(items) ? items.length : null,
|
|
sampleNetworks: Array.isArray(items)
|
|
? items.slice(0, 3).map((w) => w.network || w.chain || w.id).filter(Boolean)
|
|
: [],
|
|
error: res.ok ? undefined : body?.error?.message || body?.message || text.slice(0, 180),
|
|
};
|
|
}
|
|
|
|
async function testCobo(cobo) {
|
|
const base = (cobo.COBO_API_BASE_URL || 'https://api.dev.cobo.com/v2').replace(/\/$/, '');
|
|
const apiKey = cobo.COBO_API_KEY;
|
|
const apiSecret = cobo.COBO_API_SECRET;
|
|
if (!apiKey || !apiSecret) throw new Error('COBO_API_KEY / COBO_API_SECRET missing');
|
|
|
|
// Prefer @noble/ed25519 if available; else tweetnacl; else pure crypto createPrivateKey if possible
|
|
let signEd25519;
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
const noblePath = path.join(root, 'services/token-aggregation/node_modules/@noble/ed25519');
|
|
if (fs.existsSync(noblePath)) {
|
|
const ed = require(noblePath);
|
|
signEd25519 = async (msg, sk) => {
|
|
if (ed.signAsync) return Buffer.from(await ed.signAsync(msg, sk));
|
|
return Buffer.from(ed.sign(msg, sk));
|
|
};
|
|
}
|
|
} catch {
|
|
/* fall through */
|
|
}
|
|
if (!signEd25519) {
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
const nacl = require(path.join(root, 'services/token-aggregation/node_modules/tweetnacl'));
|
|
signEd25519 = async (msg, sk) => Buffer.from(nacl.sign.detached(msg, sk));
|
|
} catch {
|
|
/* use openssl-free path via dynamic import of noble from npm if needed */
|
|
}
|
|
}
|
|
|
|
// Install-free: use Node crypto for Ed25519 if secret is 32-byte seed
|
|
if (!signEd25519) {
|
|
signEd25519 = async (msg, skHex) => {
|
|
// Node 20+: createPrivateKey from raw JWK-like — use sodium via webcrypto experimental
|
|
// Fallback: spawn openssl not available for raw hex easily.
|
|
// Use @noble via dynamic CDN-less: implement with crypto.createPrivateKey PKCS8
|
|
throw new Error('Need @noble/ed25519 or tweetnacl — run npm install in scripts/custody');
|
|
};
|
|
}
|
|
|
|
const method = 'GET';
|
|
const pathPart = '/v2/wallets';
|
|
const params = 'limit=5';
|
|
const body = '';
|
|
const nonce = String(Date.now());
|
|
const strToSign = `${method}|${pathPart}|${nonce}|${params}|${body}`;
|
|
const contentHash = crypto.createHash('sha256').update(
|
|
crypto.createHash('sha256').update(strToSign, 'utf8').digest(),
|
|
).digest();
|
|
|
|
const sk = Buffer.from(apiSecret, 'hex');
|
|
if (sk.length !== 32) {
|
|
return {
|
|
ok: false,
|
|
http: 0,
|
|
error: `API secret hex length ${sk.length} (expected 32 bytes / 64 hex)`,
|
|
apiKeyPrefix: redact(apiKey),
|
|
};
|
|
}
|
|
|
|
// tweetnacl needs 64-byte secret key (seed+pub); @noble accepts 32-byte seed
|
|
let signatureHex;
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
let ed;
|
|
try {
|
|
ed = require('@noble/ed25519');
|
|
} catch {
|
|
ed = require(path.join(root, 'node_modules/@noble/ed25519'));
|
|
}
|
|
// noble v2 uses hashes.sha512
|
|
if (ed.etc?.sha512Async == null && ed.hashes == null) {
|
|
const { createHash } = await import('crypto');
|
|
ed.etc = ed.etc || {};
|
|
ed.etc.sha512Sync = (...m) => createHash('sha512').update(Buffer.concat(m.map((x) => Buffer.from(x)))).digest();
|
|
}
|
|
const sig = await (ed.signAsync ? ed.signAsync(contentHash, sk) : Promise.resolve(ed.sign(contentHash, sk)));
|
|
signatureHex = Buffer.from(sig).toString('hex');
|
|
} catch (e1) {
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
const nacl = require('tweetnacl');
|
|
// Derive full secret key from seed
|
|
const kp = nacl.sign.keyPair.fromSeed(sk);
|
|
signatureHex = Buffer.from(nacl.sign.detached(contentHash, kp.secretKey)).toString('hex');
|
|
} catch (e2) {
|
|
return {
|
|
ok: false,
|
|
http: 0,
|
|
error: `Ed25519 sign unavailable: ${e1.message}; ${e2.message}`,
|
|
apiKeyPrefix: redact(apiKey),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Host already includes /v2 — request URL is base + /wallets
|
|
const url = `${base}/wallets?${params}`;
|
|
const res = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Biz-Api-Key': apiKey,
|
|
'Biz-Api-Nonce': nonce,
|
|
'Biz-Api-Signature': signatureHex,
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
const text = await res.text();
|
|
let bodyJson;
|
|
try {
|
|
bodyJson = JSON.parse(text);
|
|
} catch {
|
|
bodyJson = { raw: text.slice(0, 200) };
|
|
}
|
|
|
|
const data = bodyJson?.data ?? bodyJson;
|
|
const list = data?.data ?? data?.wallets ?? (Array.isArray(data) ? data : []);
|
|
|
|
return {
|
|
ok: res.status >= 200 && res.status < 300,
|
|
http: res.status,
|
|
base,
|
|
apiKeyPrefix: redact(apiKey),
|
|
expires: cobo.COBO_EXPIRES,
|
|
walletCount: Array.isArray(list) ? list.length : data?.total ?? null,
|
|
error: res.ok
|
|
? undefined
|
|
: bodyJson?.error_message || bodyJson?.errorMessage || bodyJson?.message || text.slice(0, 220),
|
|
};
|
|
}
|
|
|
|
const dfns = loadEnvFile('secrets/dfns/credentials.env');
|
|
const cobo = loadEnvFile('secrets/cobo/credentials.env');
|
|
|
|
const dfnsResult = await testDfns(dfns);
|
|
const coboResult = await testCobo(cobo);
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
saved: {
|
|
dfnsEnv: 'secrets/dfns/credentials.env',
|
|
dfnsPem: 'secrets/dfns/rsa2048.pem',
|
|
coboEnv: 'secrets/cobo/credentials.env',
|
|
gitignored: true,
|
|
},
|
|
dfns: dfnsResult,
|
|
cobo: coboResult,
|
|
bothOk: dfnsResult.ok && coboResult.ok,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
process.exit(dfnsResult.ok && coboResult.ok ? 0 : 1);
|