2026-05-14 21:00:07 -05:00
|
|
|
/**
|
|
|
|
|
* Checks the Forgejo repo for newer releases and compares against the running
|
|
|
|
|
* package.json version. Results are cached in memory (1 hour for success,
|
|
|
|
|
* 5 minutes for errors) so the status page stays fast under load.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
// import required so Node type-strips this .cts (it has no other import).
|
|
|
|
|
import type { Db } from '../types/db';
|
|
|
|
|
|
refactor(server): migrate middleware, workers, and db layer to TypeScript (.cts)
- middleware/ (5): requireAuth, securityHeaders, errorFormatter, csrf,
rateLimiter — fully typed against the shared http Req/Res/Next types.
- workers/dailyWorker — fully typed.
- db/ (4): database, subscriptionCatalogSeed, migrations/versionedMigrations,
migrations/legacyReconcileMigrations — large dynamic schema/migration infra,
converted with @ts-nocheck (typing deferred). All requires updated to .cts.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:34:08 -05:00
|
|
|
const { getSetting } = require('../db/database.cts');
|
2026-07-03 10:05:37 -05:00
|
|
|
|
2026-05-14 21:00:07 -05:00
|
|
|
const REPO_API_BASE = process.env.REPO_API_URL
|
|
|
|
|
|| 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker';
|
|
|
|
|
|
2026-07-03 10:05:37 -05:00
|
|
|
// QA-B16-01: the version check is opt-out-able. Admins can disable the external
|
|
|
|
|
// request via the `update_check_enabled` setting (default on). When off, no
|
|
|
|
|
// network call is made and a `disabled` status is returned.
|
2026-07-06 10:47:16 -05:00
|
|
|
function updateCheckEnabled(): boolean {
|
2026-07-03 10:05:37 -05:00
|
|
|
return getSetting('update_check_enabled') !== 'false';
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 21:00:07 -05:00
|
|
|
const TTL_OK_MS = 60 * 60 * 1000; // 1 hour on success
|
|
|
|
|
const TTL_ERROR_MS = 5 * 60 * 1000; // 5 min on error (avoid hammering)
|
|
|
|
|
const FETCH_TIMEOUT_MS = 8_000;
|
|
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
let _cache: { result: any; expiresAt: number } = { result: null, expiresAt: 0 };
|
|
|
|
|
let _pkg: any = null;
|
2026-05-14 21:00:07 -05:00
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
function getCurrentVersion(): string {
|
2026-05-14 21:00:07 -05:00
|
|
|
if (!_pkg) {
|
|
|
|
|
try { _pkg = require('../package.json'); } catch { _pkg = { version: '0.0.0' }; }
|
|
|
|
|
}
|
|
|
|
|
return _pkg.version;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns positive if a > b, negative if a < b, 0 if equal.
|
2026-07-06 10:47:16 -05:00
|
|
|
function compareVersions(a: string, b: string): number {
|
|
|
|
|
const parse = (v: string) => String(v).replace(/^v/, '').split('.').map(Number);
|
2026-05-14 21:00:07 -05:00
|
|
|
const pa = parse(a), pb = parse(b);
|
|
|
|
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
|
|
|
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
|
|
|
if (diff !== 0) return diff;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-06 10:47:16 -05:00
|
|
|
* @param force Skip the cache and always hit the API.
|
|
|
|
|
* @returns Update status object.
|
2026-05-14 21:00:07 -05:00
|
|
|
*/
|
2026-07-06 10:47:16 -05:00
|
|
|
async function checkForUpdates(force = false): Promise<any> {
|
2026-05-14 21:00:07 -05:00
|
|
|
const now = Date.now();
|
|
|
|
|
|
2026-07-03 10:05:37 -05:00
|
|
|
// Opt-out: no external request when disabled by the admin.
|
|
|
|
|
if (!updateCheckEnabled()) {
|
|
|
|
|
return {
|
|
|
|
|
current_version: getCurrentVersion(),
|
|
|
|
|
latest_version: null,
|
|
|
|
|
up_to_date: null,
|
|
|
|
|
has_update: false,
|
|
|
|
|
latest_release_url: null,
|
|
|
|
|
published_at: null,
|
|
|
|
|
last_checked_at: null,
|
|
|
|
|
error: null,
|
|
|
|
|
disabled: true,
|
|
|
|
|
cached: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 21:00:07 -05:00
|
|
|
if (!force && _cache.result && now < _cache.expiresAt) {
|
|
|
|
|
return { ..._cache.result, cached: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentVersion = getCurrentVersion();
|
|
|
|
|
const checkedAt = new Date().toISOString();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${REPO_API_BASE}/releases/latest`, {
|
|
|
|
|
headers: {
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
'User-Agent': `BillTracker/${currentVersion} UpdateCheck`,
|
|
|
|
|
},
|
|
|
|
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 404 = no releases published yet — treat as up-to-date
|
|
|
|
|
if (res.status === 404) {
|
|
|
|
|
const result = {
|
|
|
|
|
current_version: currentVersion,
|
|
|
|
|
latest_version: null,
|
|
|
|
|
up_to_date: true,
|
|
|
|
|
has_update: false,
|
|
|
|
|
latest_release_url: null,
|
|
|
|
|
published_at: null,
|
|
|
|
|
last_checked_at: checkedAt,
|
|
|
|
|
error: null,
|
|
|
|
|
cached: false,
|
|
|
|
|
};
|
|
|
|
|
_cache = { result, expiresAt: now + TTL_OK_MS };
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!res.ok) throw new Error(`Forgejo API returned HTTP ${res.status}`);
|
|
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
const release: any = await res.json();
|
2026-05-14 21:00:07 -05:00
|
|
|
const rawTag = release.tag_name || '';
|
|
|
|
|
const latestVersion = rawTag.replace(/^v/, '') || null;
|
|
|
|
|
const hasUpdate = latestVersion
|
|
|
|
|
? compareVersions(currentVersion, latestVersion) < 0
|
|
|
|
|
: false;
|
|
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
|
current_version: currentVersion,
|
|
|
|
|
latest_version: latestVersion,
|
|
|
|
|
up_to_date: !hasUpdate,
|
|
|
|
|
has_update: hasUpdate,
|
|
|
|
|
latest_release_url: release.html_url || null,
|
|
|
|
|
published_at: release.published_at || null,
|
|
|
|
|
last_checked_at: checkedAt,
|
|
|
|
|
error: null,
|
|
|
|
|
cached: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_cache = { result, expiresAt: now + TTL_OK_MS };
|
|
|
|
|
return result;
|
|
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
} catch (err: any) {
|
2026-05-14 21:00:07 -05:00
|
|
|
const result = {
|
|
|
|
|
current_version: currentVersion,
|
|
|
|
|
latest_version: null,
|
|
|
|
|
up_to_date: null, // unknown — network or API failure
|
|
|
|
|
has_update: false,
|
|
|
|
|
latest_release_url: null,
|
|
|
|
|
published_at: null,
|
|
|
|
|
last_checked_at: checkedAt,
|
|
|
|
|
error: err.message || 'Update check failed',
|
|
|
|
|
cached: false,
|
|
|
|
|
};
|
|
|
|
|
_cache = { result, expiresAt: now + TTL_ERROR_MS };
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 10:05:37 -05:00
|
|
|
module.exports = { checkForUpdates, updateCheckEnabled };
|