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
29 changes: 28 additions & 1 deletion apps/api/src/lib/build-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,31 @@ function readApiPackageJson(): { version: string } {
return { version: parsed.version };
}

export const apiVersion = readApiPackageJson().version;
function readGitCommit(): string | undefined {
try {
const gitPath = path.join(API_PACKAGE_ROOT, ".git", "HEAD");
if (!fs.existsSync(gitPath)) {
return undefined;
}
const head = fs.readFileSync(gitPath, "utf-8").trim();
if (head.startsWith("ref:")) {
const refPath = path.join(API_PACKAGE_ROOT, ".git", head.slice(5).trim());
if (fs.existsSync(refPath)) {
return fs.readFileSync(refPath, "utf-8").trim().slice(0, 7);
}
} else {
return head.slice(0, 7);
}
} catch {
return undefined;
}
}

const packageInfo = readApiPackageJson();

export const apiVersion = packageInfo.version;
export const buildMetadata = {
version: packageInfo.version,
gitCommit: readGitCommit(),
buildTime: process.env.BUILD_TIME
};
42 changes: 2 additions & 40 deletions apps/api/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,44 +78,6 @@ export const config = {
analyticsStorage: parsed.data.ANALYTICS_STORAGE
};

/**
* A sanitized snapshot of the deployment configuration.
* Contains only booleans and safe enum values — never secrets, private keys, or auth headers.
*/
export interface ConfigSnapshot {
/** Stellar network identifier, e.g. "stellar:testnet" or "stellar:pubnet" */
network: string;
/** Whether the API is running in demo mode (no real payments) */
demoMode: boolean;
/** Whether an x402 facilitator URL is configured */
facilitatorConfigured: boolean;
/** Whether an x402 facilitator API key is configured (value never exposed) */
facilitatorApiKeyConfigured: boolean;
/** Whether a pay-to Stellar address is configured */
payToConfigured: boolean;
/** Whether sponsorship/subsidy mode is enabled */
sponsorshipEnabled: boolean;
/** Whether a sponsorship signing secret is configured (value never exposed) */
sponsorshipSigningSecretConfigured: boolean;
/** Whether at least one search/AI provider API key is configured (values never exposed) */
anyProviderKeyConfigured: boolean;
}

/**
* Returns a sanitized snapshot of the current deployment configuration.
* Safe to include in public health/diagnostics responses — no secrets are returned.
*/
export function getConfigSnapshot(): ConfigSnapshot {
return {
network: config.STELLAR_NETWORK,
demoMode: config.demoMode,
facilitatorConfigured: Boolean(config.X402_FACILITATOR_URL),
facilitatorApiKeyConfigured: Boolean(config.X402_FACILITATOR_API_KEY),
payToConfigured: Boolean(config.X402_PAY_TO_ADDRESS),
sponsorshipEnabled: config.sponsorshipEnabled,
sponsorshipSigningSecretConfigured: Boolean(config.SPONSORSHIP_SIGNING_SECRET),
anyProviderKeyConfigured: Boolean(
config.BRAVE_API_KEY || config.SERPAPI_API_KEY || config.NEWS_API_KEY || config.GROQ_API_KEY
)
};
export function getFacilitatorConfigured(): boolean {
return !!parsed.data.X402_FACILITATOR_URL && !!parsed.data.X402_FACILITATOR_API_KEY && parsed.data.X402_FACILITATOR_API_KEY.length > 0;
}
57 changes: 57 additions & 0 deletions apps/api/src/lib/facilitator-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
applyApiTestEnv,
resetApiTestStorage
} from "../test/api-test-helpers.js";

describe("facilitator-check", () => {
let analyticsDbPath: string;

beforeEach(() => {
({ analyticsDbPath } = applyApiTestEnv());
});

afterEach(async () => {
await resetApiTestStorage(analyticsDbPath);
});

it("returns false in demo mode without making network requests", async () => {
const { checkFacilitatorSupported } = await import(
"../lib/facilitator-check.js"
);

const result = await checkFacilitatorSupported();

expect(result.ok).toBe(false);
expect(result.error).toBeUndefined();
});

it("returns cached false when no API key is configured", async () => {
const { checkFacilitatorSupported } = await import(
"../lib/facilitator-check.js"
);

const result = await checkFacilitatorSupported();

expect(result.ok).toBe(false);
expect(result).not.toHaveProperty("apiKey");
expect(result).not.toHaveProperty("secret");
});

it("clearFacilitatorCache resets cached state", async () => {
const { checkFacilitatorSupported, clearFacilitatorCache } = await import(
"../lib/facilitator-check.js"
);

clearFacilitatorCache();
await checkFacilitatorSupported();

let result = await checkFacilitatorSupported();
expect(result.ok).toBe(false);

clearFacilitatorCache();

result = await checkFacilitatorSupported();
expect(result.ok).toBe(false);
});
});
91 changes: 91 additions & 0 deletions apps/api/src/lib/facilitator-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { config } from "./config.js";

interface FacilitatorCheckResult {
ok: boolean;
checkedAt: string;
error?: string;
}

let cachedResult: FacilitatorCheckResult | null = null;
let cachedAt: number = 0;
let pendingCheck: Promise<FacilitatorCheckResult> | null = null;

const CACHE_TTL_MS = 30_000;

export async function checkFacilitatorSupported(): Promise<FacilitatorCheckResult> {
const now = Date.now();

if (cachedResult && now - cachedAt < CACHE_TTL_MS) {
return cachedResult;
}

if (pendingCheck) {
return pendingCheck;
}

if (config.demoMode || !config.X402_FACILITATOR_API_KEY) {
cachedResult = { ok: false, checkedAt: new Date().toISOString() };
cachedAt = now;
return cachedResult;
}

pendingCheck = performCheck();

try {
cachedResult = await pendingCheck;
cachedAt = now;
return cachedResult;
} finally {
pendingCheck = null;
}
}

async function performCheck(): Promise<FacilitatorCheckResult> {
try {
const controller = new AbortController();
const timeoutMs = 5000;
const timeout = setTimeout(() => controller.abort(), timeoutMs);

const response = await fetch(
`${config.X402_FACILITATOR_URL.replace(/\/+$/, "")}/supported`,
{
method: "GET",
signal: controller.signal,
headers: config.X402_FACILITATOR_API_KEY
? { Authorization: `Bearer ${config.X402_FACILITATOR_API_KEY}` }
: undefined
}
);

clearTimeout(timeout);

if (!response.ok) {
return {
ok: false,
checkedAt: new Date().toISOString(),
error: `HTTP ${response.status}`
};
}

return { ok: true, checkedAt: new Date().toISOString() };
} catch (error) {
const message =
error instanceof Error
? error.name === "AbortError"
? "timeout"
: error.message
: String(error);

return {
ok: false,
checkedAt: new Date().toISOString(),
error: message
};
}
}

export function clearFacilitatorCache(): void {
cachedResult = null;
cachedAt = 0;
pendingCheck = null;
}
Loading