feat: expose immutable release identity

This commit is contained in:
jwinterm
2026-07-23 13:27:17 -04:00
parent 578f6ce1a0
commit 6a3d5f8191
7 changed files with 203 additions and 1 deletions
+71
View File
@@ -0,0 +1,71 @@
const fs = require('fs');
const path = require('path');
const RELEASE_ID_PATTERN = /^git-([0-9a-f]{12})$/;
const COMMIT_PATTERN = /^[0-9a-f]{40}$/;
const MAX_FILE_BYTES = 128;
function readBoundedRegularFile(filePath) {
const descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW
);
try {
const before = fs.fstatSync(descriptor);
if (
!before.isFile()
|| before.nlink !== 1
|| (before.mode & 0o222) !== 0
|| before.size < 1
|| before.size > MAX_FILE_BYTES
) {
throw new Error('release identity file has unsafe shape');
}
const value = fs.readFileSync(descriptor, { encoding: 'utf8' }).trim();
const after = fs.fstatSync(descriptor);
if (
before.dev !== after.dev
|| before.ino !== after.ino
|| before.size !== after.size
|| before.mtimeMs !== after.mtimeMs
) {
throw new Error('release identity file changed while it was read');
}
return value;
} finally {
fs.closeSync(descriptor);
}
}
function unverifiedIdentity() {
return Object.freeze({ verified: false, id: null, commit: null });
}
function loadReleaseIdentity(options = {}) {
const production = options.production === undefined
? process.env.NODE_ENV === 'production'
: Boolean(options.production);
const releaseRoot = options.releaseRoot || path.resolve(__dirname, '..', '..');
try {
const id = readBoundedRegularFile(path.join(releaseRoot, '.release-id'));
const commit = readBoundedRegularFile(path.join(releaseRoot, '.release-commit'));
const idMatch = id.match(RELEASE_ID_PATTERN);
if (!idMatch || !COMMIT_PATTERN.test(commit) || idMatch[1] !== commit.slice(0, 12)) {
throw new Error('release identity files disagree');
}
return Object.freeze({ verified: true, id, commit });
} catch (error) {
if (production) {
throw new Error('production release identity is absent or invalid', { cause: error });
}
return unverifiedIdentity();
}
}
module.exports = {
COMMIT_PATTERN,
RELEASE_ID_PATTERN,
loadReleaseIdentity,
readBoundedRegularFile
};
+5
View File
@@ -2416,6 +2416,11 @@ class GameModeManager {
}));
return {
release: this.releaseIdentity || {
verified: false,
id: null,
commit: null
},
operatedProductProfileId: getOperatedProductProfile(process.env)?.id || null,
mode: this.gameMode,
freePlayEnabled: !!this.freePlayEnabled, // players may choose free play even on a paid instance
+5
View File
@@ -34,6 +34,7 @@ const GameModeManager = require('./game/gameModeManager');
const PaymentConfigManager = require('./config/paymentConfig');
const EnvironmentValidator = require('./config/environmentValidator');
const RuntimePolicy = require('./config/runtimePolicy');
const { loadReleaseIdentity } = require('./config/releaseIdentity');
const asyncHandler = require('./middleware/asyncHandler');
const createErrorMiddleware = require('./middleware/errorHandler');
const noStore = require('./middleware/noStore');
@@ -58,7 +59,9 @@ const { isSmirkEnabled } = require('./auth/smirkPolicy');
// Initialize payment configuration
const paymentConfigManager = new PaymentConfigManager({ logger: console });
const environmentValidator = new EnvironmentValidator({ logger: console });
let releaseIdentity;
try {
releaseIdentity = loadReleaseIdentity();
environmentValidator.assertValid(paymentConfigManager.getConfig());
} catch (error) {
console.error(`❌ Startup aborted: ${error.message}`);
@@ -79,6 +82,7 @@ const walletRPCService = new WalletRPCService(debugManager, {
minConfirmations: paymentConfigManager.getConfig().payouts.processing.confirmations
});
const gameModeManager = new GameModeManager(databaseManager, walletRPCService, debugManager, paymentConfigManager);
gameModeManager.releaseIdentity = releaseIdentity;
// Provide io reference so GameModeManager can emit events (e.g., credits_update)
gameModeManager.io = io;
@@ -473,6 +477,7 @@ function publicHealthSnapshot() {
queuedGames: socketHandlers?.queueManager?.getQueueLength?.() || 0,
connectedPlayers: io.sockets.sockets.size || 0,
gameMode: gameModeManager.gameMode,
releaseIdentity,
uptime: process.uptime()
});
// Informational only: an optional accounting sink must not make gameplay readiness fail.
+15
View File
@@ -1,5 +1,19 @@
/** Build the public, non-sensitive health payload used by probes and the status UI. */
function buildPublicHealth(input = {}) {
const releaseCandidate = input.releaseIdentity;
const releaseValid = releaseCandidate?.verified === true
&& /^git-[0-9a-f]{12}$/.test(releaseCandidate.id || '')
&& /^[0-9a-f]{40}$/.test(releaseCandidate.commit || '')
&& releaseCandidate.id.slice(4) === releaseCandidate.commit.slice(0, 12);
const release = releaseValid ? {
verified: true,
id: releaseCandidate.id,
commit: releaseCandidate.commit
} : {
verified: false,
id: null,
commit: null
};
const sanitizeIdentity = (identity, required) => {
const pair = (value) => value && typeof value === 'object' ? {
cryptoType: typeof value.cryptoType === 'string' ? value.cryptoType : null,
@@ -73,6 +87,7 @@ function buildPublicHealth(input = {}) {
paymentsEnabled,
payoutsEnabled
},
release,
identities: {
daemon: sanitizeIdentity(daemonIdentity, daemonIdentityRequired),
wallet: sanitizeIdentity(input.walletIdentity, walletIdentityRequired)
+10
View File
@@ -228,6 +228,16 @@ describe('GameModeManager', () => {
}
});
test('game mode info binds Socket.IO clients to the immutable release', () => {
const release = {
verified: true,
id: `git-${'a'.repeat(12)}`,
commit: 'a'.repeat(40)
};
gmm.releaseIdentity = release;
expect(gmm.getGameModeInfo().release).toEqual(release);
});
test('crypto race economy is hidden when the payout master switch is off', () => {
const keys = [
'MATCH_ENABLED', 'MATCH_CRYPTO_RACE_ENABLED', 'MATCH_PAYOUTS_ENABLED',
+33 -1
View File
@@ -16,7 +16,39 @@ describe('public health payload', () => {
status: 'ok',
ready: true,
checks: { database: 'up', chain: 'up', wallet: 'up' },
money: { paymentsEnabled: true, payoutsEnabled: false }
money: { paymentsEnabled: true, payoutsEnabled: false },
release: { verified: false, id: null, commit: null }
});
});
test('publishes only a self-consistent immutable release identity', () => {
const commit = 'a'.repeat(40);
const health = buildPublicHealth({
databaseReady: true,
blockHeight: 123,
releaseIdentity: {
verified: true,
id: `git-${commit.slice(0, 12)}`,
commit
}
});
expect(health.release).toEqual({
verified: true,
id: `git-${commit.slice(0, 12)}`,
commit
});
const mismatched = buildPublicHealth({
releaseIdentity: {
verified: true,
id: `git-${'b'.repeat(12)}`,
commit
}
});
expect(mismatched.release).toEqual({
verified: false,
id: null,
commit: null
});
});
+64
View File
@@ -0,0 +1,64 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
loadReleaseIdentity,
readBoundedRegularFile
} = require('../src/config/releaseIdentity');
describe('immutable release identity', () => {
let root;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'wowngeon-release-identity-'));
});
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});
function writeIdentity(commit = 'a'.repeat(40), id = `git-${commit.slice(0, 12)}`) {
fs.writeFileSync(path.join(root, '.release-id'), `${id}\n`, { mode: 0o444 });
fs.writeFileSync(path.join(root, '.release-commit'), `${commit}\n`, { mode: 0o444 });
}
test('returns the matching artifact release ID and commit', () => {
writeIdentity();
expect(loadReleaseIdentity({ releaseRoot: root, production: true })).toEqual({
verified: true,
id: `git-${'a'.repeat(12)}`,
commit: 'a'.repeat(40)
});
});
test('fails production closed when identity files disagree', () => {
writeIdentity('b'.repeat(40), `git-${'c'.repeat(12)}`);
expect(() => loadReleaseIdentity({ releaseRoot: root, production: true }))
.toThrow('production release identity is absent or invalid');
});
test('fails production closed when either identity file is absent', () => {
expect(() => loadReleaseIdentity({ releaseRoot: root, production: true }))
.toThrow('production release identity is absent or invalid');
});
test('returns an explicit unverified identity outside production', () => {
expect(loadReleaseIdentity({ releaseRoot: root, production: false })).toEqual({
verified: false,
id: null,
commit: null
});
});
test('refuses symlinks and oversized identity files', () => {
const target = path.join(root, 'target');
fs.writeFileSync(target, 'a'.repeat(40));
const linked = path.join(root, 'linked');
fs.symlinkSync(target, linked);
expect(() => readBoundedRegularFile(linked)).toThrow();
const oversized = path.join(root, 'oversized');
fs.writeFileSync(oversized, 'x'.repeat(129));
expect(() => readBoundedRegularFile(oversized)).toThrow('unsafe shape');
});
});