feat(public): version/health surfacing for the app first-run probe

Expose a small backend identity/version descriptor (§8.4 of the Android plan)
so a client can positively recognize a Runic Gateway backend on first-run and
run a version-mismatch guard, instead of inferring from an incidental shape.

- New config/version.js: { service: 'runic-gateway', api: 'v1', server: <pkg> }.
- GET /public/status now includes a `version` block (the app already calls this
  on first-run, so it gets identity + version in one round trip).
- New GET /public/version: a lightweight, DB-free identity endpoint — the
  canonical target for the version guard and a cheap liveness check.
- Swagger: PublicVersion schema + version on PublicStatus; /version annotated.
- test/publicVersion.test.js covers the config shape and the DB-free 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
2026-07-19 11:47:26 -05:00
parent 93a2c0d55f
commit 90c8eae20f
6 changed files with 182 additions and 2 deletions

View File

@@ -0,0 +1,21 @@
// ── Public API version / identity ──────────────────────────────────────────
//
// A tiny, dependency-free descriptor of this backend, surfaced on GET
// /public/status and GET /public/version. A client (notably the Android app)
// uses it to:
// • positively recognize a Runic Gateway backend on first-run (the `service`
// tag), instead of guessing from an incidental response shape; and
// • run a version-mismatch guard — compare `api` against the contract the
// client was built against and surface a clear "update required" state
// rather than mis-parsing a future, changed response.
//
// `api` is the coarse contract version (bumped only on a breaking re-shape, which
// would be a v2 mount); `server` is the informational package version.
const pkg = require('../../package.json')
module.exports = {
service: 'runic-gateway', // stable backend identifier for first-run detection
api: 'v1', // API contract version (matches the /api/v1 mount)
server: pkg.version || '0.0.0', // server package version (informational)
}

View File

@@ -3,6 +3,7 @@ const wiki = require('../../../model/wiki/wiki.model')
const settings = require('../../../model/settings/settings.model')
const pages = require('../../../model/pages/pages.model')
const mailer = require('../../../utils/mailer')
const version = require('../../../config/version')
const { getUserFromRequest } = require('../../../utils/auth')
const token = require('../../../auth/token')
@@ -29,12 +30,22 @@ async function getStatus(req, res) {
return res.json({
mode: (await settings.get('site_mode')) || 'live',
status_message: (await settings.get('status_message')) || '',
// Identity + version, so a client's first-run probe recognizes a Runic
// Gateway backend and can run its version-mismatch guard in this one call.
version,
})
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// Lightweight, DB-free identity/version endpoint — the canonical target for a
// client's first-run recognition and its periodic version-mismatch guard, without
// touching settings or the database. Also serves as a cheap liveness check.
function getVersion(req, res) {
return res.json(version)
}
async function getPosts(req, res) {
const { category } = req.params
if (!posts.isValidUrlCategory(category)) {
@@ -153,6 +164,7 @@ async function contact(req, res) {
module.exports = {
getSettings,
getStatus,
getVersion,
getPosts,
getPost,
getWikiCategories,

View File

@@ -22,10 +22,18 @@ publicRouter.get(
'/status',
// #swagger.tags = ['Public']
// #swagger.summary = 'Site mode / status'
// #swagger.description = 'Current site mode (live or maintenance) so the client can show the maintenance page.'
// #swagger.description = 'Current site mode (live or maintenance) so the client can show the maintenance page, plus a version block (service id + API/server versions) for a client first-run probe and version-mismatch guard.'
/* #swagger.responses[200] = { description: 'Site status', content: { "application/json": { schema: { $ref: "#/components/schemas/PublicStatus" } } } } */
ctrl.getStatus,
)
publicRouter.get(
'/version',
// #swagger.tags = ['Public']
// #swagger.summary = 'Backend identity + version'
// #swagger.description = 'Lightweight, DB-free descriptor of this backend: a stable service id and the API/server versions. A client uses it to recognize a Runic Gateway backend on first-run and to run a version-mismatch guard. Doubles as a cheap liveness check.'
/* #swagger.responses[200] = { description: 'Backend version', content: { "application/json": { schema: { $ref: "#/components/schemas/PublicVersion" } } } } */
ctrl.getVersion,
)
publicRouter.post(
'/contact',
// #swagger.tags = ['Public']