diff --git a/packages/cli/src/config/env.ts b/packages/cli/src/config/env.ts new file mode 100644 index 0000000..9e7b8cd --- /dev/null +++ b/packages/cli/src/config/env.ts @@ -0,0 +1,6 @@ +export const STELLAR_EXPLAIN_TOKEN_ENV = "STELLAR_EXPLAIN_TOKEN"; + +export function getEnvApiToken(env: NodeJS.ProcessEnv = process.env): string | undefined { + const token = env[STELLAR_EXPLAIN_TOKEN_ENV]?.trim(); + return token || undefined; +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 168856b..442fb64 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -8,7 +8,7 @@ import { registerHealth } from "./commands/health.js"; import { registerBatch } from "./commands/batch.js"; import { registerExplain } from "./commands/explain.js"; import { registerWatch } from "./commands/watch.js"; -import { registerCompletion } from "./commands/completion.js"; +import { registerCompletionCommand } from "./commands/completion.js"; import { registerConfigSet } from "./commands/configSet.js"; import { registerConfigGet } from "./commands/configGet.js"; import { registerConfigList } from "./commands/configList.js"; @@ -19,36 +19,9 @@ import { readConfigFile } from "./lib/configFile.js"; import { getCliVersion } from "./lib/pkgVersion.js"; const program = new Command(); +const version = getCliVersion(); program - .name('stellar-explain') - .description('CLI for exploring and explaining Stellar transactions and accounts') - .version('0.1.0'); - -// ── Existing commands (stubs shown; replace with real implementations) ──────── - -program - .command('tx ') - .description('Look up and explain a Stellar transaction') - .action((hash: string) => { - addEntry('tx', hash); - // TODO: delegate to tx command handler - console.log(`Looking up transaction: ${hash}`); - }); - -program - .command('account ') - .description('Look up and explain a Stellar account') - .action((id: string) => { - addEntry('account', id); - // TODO: delegate to account command handler - console.log(`Looking up account: ${id}`); - }); - -registerHistoryCommand(program); // #441 / #442 / #443 -registerCompletionCommand(program); // #440 - -program.parse(process.argv); .name(BIN_NAME) .version(version) .option("--url ", "API base URL") @@ -75,7 +48,7 @@ registerHealth(program); registerBatch(program); registerExplain(program); registerWatch(program); -registerCompletion(program); +registerCompletionCommand(program); registerConfigSet(program); registerConfigGet(program); registerConfigList(program); diff --git a/packages/cli/src/lib/client.ts b/packages/cli/src/lib/client.ts index 0581453..c0e8791 100644 --- a/packages/cli/src/lib/client.ts +++ b/packages/cli/src/lib/client.ts @@ -4,12 +4,14 @@ import type { AccountExplanation, HealthResponse, } from "../types/index.js"; +import { getEnvApiToken } from "../config/env.js"; export interface ClientOptions { baseUrl: string; timeout: number; // #276 verbose: boolean; // #277 retries: number; // #414 + apiToken?: string; } async function requestOnce( @@ -19,9 +21,11 @@ async function requestOnce( const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), opts.timeout); const start = Date.now(); + const apiToken = opts.apiToken ?? getEnvApiToken(); + const headers = apiToken ? { Authorization: `Bearer ${apiToken}` } : undefined; try { - const res = await fetch(url, { signal: controller.signal }); + const res = await fetch(url, { signal: controller.signal, headers }); const duration = Date.now() - start; if (opts.verbose) { diff --git a/packages/cli/tests/client.test.ts b/packages/cli/tests/client.test.ts index 815065f..cfd02ec 100644 --- a/packages/cli/tests/client.test.ts +++ b/packages/cli/tests/client.test.ts @@ -27,6 +27,7 @@ describe("createClient", () => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllEnvs(); }); // ── Success cases ────────────────────────────────────────────── @@ -90,6 +91,37 @@ describe("createClient", () => { expect.objectContaining({ signal: expect.any(AbortSignal) }), ); }); + + it("sends STELLAR_EXPLAIN_TOKEN as a Bearer token", async () => { + vi.stubEnv("STELLAR_EXPLAIN_TOKEN", "secret-token"); + vi.stubGlobal("fetch", mockFetchResponse(200, { status: "ok" })); + + const client = createClient(defaultOpts); + await client.getHealth(); + + expect(fetch).toHaveBeenCalledWith( + "http://localhost:4000/health", + expect.objectContaining({ + headers: { Authorization: "Bearer secret-token" }, + signal: expect.any(AbortSignal), + }), + ); + }); + + it("prefers an explicit apiToken over STELLAR_EXPLAIN_TOKEN", async () => { + vi.stubEnv("STELLAR_EXPLAIN_TOKEN", "env-token"); + vi.stubGlobal("fetch", mockFetchResponse(200, { status: "ok" })); + + const client = createClient({ ...defaultOpts, apiToken: "option-token" }); + await client.getHealth(); + + expect(fetch).toHaveBeenCalledWith( + "http://localhost:4000/health", + expect.objectContaining({ + headers: { Authorization: "Bearer option-token" }, + }), + ); + }); }); // ── 404 Not Found ──────────────────────────────────────────────