From f03bb7ca27ab975b4921f7d7e7b2719837b791eb Mon Sep 17 00:00:00 2001 From: xcel Date: Tue, 25 Aug 2026 23:51:50 +0100 Subject: [PATCH] routes: source /meta version from package.json, add build and network info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /meta hardcoded name/version/description as literals, duplicating package.json and guaranteed to drift on the first release. It also still described the service as a "scaffold". - src/meta/version.ts reads name and version from package.json at request time (cached after first read) instead of a literal. - Adds build.commit and build.builtAt, sourced from BUILD_SHA / BUILD_TIME environment variables rather than shelling out to git — the deployed container has no .git directory. Both default to "unknown" when unset. .env.example documents them as CI-injected (e.g. BUILD_SHA=${{ github.sha }}). - Adds network.stellarNetwork, network.sorobanRpcUrl, and the three configured contract addresses, so integrators hitting /meta can confirm which environment they're pointed at. - Rewrites the description for a shipped service; no "scaffold" wording remains. Tests cover version sourcing from package.json, the unknown/injected build-info cases, network/contract reporting, and the absence of "scaffold" in the description. Note: this touches the same /meta handler in src/routes/v1/index.ts as PR #58 (dispute routes), which adds a route registration line right below it. The two changes are independent, but merging both will need a small one-hunk conflict resolution in that file depending on merge order — flagging it up front rather than leaving it as a surprise. Closes #10. --- .env.example | 6 +++ src/meta/version.test.ts | 48 +++++++++++++++++++++++ src/meta/version.ts | 85 ++++++++++++++++++++++++++++++++++++++++ src/routes/v1/index.ts | 8 ++-- 4 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src/meta/version.test.ts create mode 100644 src/meta/version.ts diff --git a/.env.example b/.env.example index 9885824..a3f8796 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,12 @@ AUDIT_REGISTRY_CONTRACT_ID=CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA USAGE_METER_CONTRACT_ID=CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB PAYMENT_ROUTER_CONTRACT_ID=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +# Reported by GET /meta so a deployed instance can be identified. Injected by +# CI at build time (e.g. BUILD_SHA=${{ github.sha }}) — the running +# container has no .git directory, so these are never derived at runtime. +# BUILD_SHA= +# BUILD_TIME= + # --- Signing key custody (docs/adr/0001-soroban-signing-key-custody.md) --- # This service NEVER signs value movement. Settlement is returned to the caller # as an unsigned envelope. The only key that may exist here is a low-privilege diff --git a/src/meta/version.test.ts b/src/meta/version.test.ts new file mode 100644 index 0000000..c5c13ae --- /dev/null +++ b/src/meta/version.test.ts @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { readFileSync } from "node:fs"; +import { getMetaInfo, type MetaConfig } from "./version.js"; + +const config: MetaConfig = { + stellar: { + network: "testnet", + sorobanRpcUrl: "https://soroban-testnet.stellar.org", + contracts: { + auditRegistry: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + usageMeter: "CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + paymentRouter: "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + }, + }, +}; + +test("version is sourced from package.json, not a literal", () => { + const pkg = JSON.parse(readFileSync("package.json", "utf-8")) as { version: string }; + const info = getMetaInfo(config, {}); + assert.equal(info.version, pkg.version); +}); + +test("build commit and time default to 'unknown' when not injected", () => { + const info = getMetaInfo(config, {}); + assert.equal(info.build.commit, "unknown"); + assert.equal(info.build.builtAt, "unknown"); +}); + +test("build commit and time are read from the environment when present", () => { + const info = getMetaInfo(config, { + BUILD_SHA: "abc123", + BUILD_TIME: "2026-08-25T00:00:00.000Z", + }); + assert.equal(info.build.commit, "abc123"); + assert.equal(info.build.builtAt, "2026-08-25T00:00:00.000Z"); +}); + +test("reports the configured network and contract addresses", () => { + const info = getMetaInfo(config, {}); + assert.equal(info.network.stellarNetwork, "testnet"); + assert.equal(info.network.contracts.auditRegistry, config.stellar.contracts.auditRegistry); +}); + +test("description no longer reads as a scaffold", () => { + const info = getMetaInfo(config, {}); + assert.ok(!/scaffold/i.test(info.description)); +}); diff --git a/src/meta/version.ts b/src/meta/version.ts new file mode 100644 index 0000000..0ae2434 --- /dev/null +++ b/src/meta/version.ts @@ -0,0 +1,85 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +// src/meta/version.ts compiles to dist/meta/version.js, two levels below +// the repo root where package.json lives. +const packageJsonPath = join(here, "..", "..", "package.json"); + +interface PackageJson { + name: string; + version: string; +} + +let cachedPackageJson: PackageJson | undefined; + +function readPackageJson(): PackageJson { + if (!cachedPackageJson) { + cachedPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as PackageJson; + } + return cachedPackageJson; +} + +export interface MetaInfo { + name: string; + version: string; + description: string; + build: { + /** Commit SHA the running instance was built from. "unknown" outside CI. */ + commit: string; + /** ISO timestamp of the build. "unknown" outside CI. */ + builtAt: string; + }; + network: { + stellarNetwork: string; + sorobanRpcUrl: string; + contracts: { + auditRegistry: string; + usageMeter: string; + paymentRouter: string; + }; + }; +} + +export interface MetaConfig { + stellar: { + network: string; + sorobanRpcUrl: string; + contracts: { + auditRegistry: string; + usageMeter: string; + paymentRouter: string; + }; + }; +} + +/** + * Builds the /meta response. Version comes from package.json rather than a + * literal so it can't drift. Commit SHA and build time come from + * environment variables — the container this runs in has no .git + * directory, so they must be injected at build/deploy time (e.g. a CI step + * exporting BUILD_SHA=${{ github.sha }}), not read via `git rev-parse` at + * runtime. + */ +export function getMetaInfo(config: MetaConfig, env: NodeJS.ProcessEnv = process.env): MetaInfo { + const pkg = readPackageJson(); + return { + name: pkg.name, + version: pkg.version, + description: "REST facade for Soroban contracts and indexers.", + build: { + commit: env.BUILD_SHA ?? "unknown", + builtAt: env.BUILD_TIME ?? "unknown", + }, + network: { + stellarNetwork: config.stellar.network, + sorobanRpcUrl: config.stellar.sorobanRpcUrl, + contracts: { + auditRegistry: config.stellar.contracts.auditRegistry, + usageMeter: config.stellar.contracts.usageMeter, + paymentRouter: config.stellar.contracts.paymentRouter, + }, + }, + }; +} diff --git a/src/routes/v1/index.ts b/src/routes/v1/index.ts index e283fad..4922833 100644 --- a/src/routes/v1/index.ts +++ b/src/routes/v1/index.ts @@ -1,11 +1,9 @@ import type { FastifyPluginAsync } from "fastify"; +import { config } from "../../config/env.js"; +import { getMetaInfo } from "../../meta/version.js"; export const v1Routes: FastifyPluginAsync = async (app) => { - app.get("/meta", async () => ({ - name: "modeltrace-api", - version: "0.1.0", - description: "REST facade for Soroban contracts and indexers (scaffold).", - })); + app.get("/meta", async () => getMetaInfo(config)); // TODO: routes for contract invocation prep, webhook ingestion, admin ops };