fix: enforce operated paid mode contracts

This commit is contained in:
jwinterm
2026-07-23 20:31:20 -04:00
parent 05197925d9
commit ca8e189b20
14 changed files with 168 additions and 27 deletions
+7 -1
View File
@@ -51,6 +51,7 @@ OPERATOR_NAME=Such Software
OPERATOR_CONTACT_URL=mailto:apps@such.software
OPERATOR_CONTACT_LABEL=apps@such.software
FREE_PLAY_ENABLED=true # offer free play as a choice (Pleb board)
GAME_MODE=PAID_CREDITS # exact operated-profile legacy/socket identity
PAYMENT_MODES=credits # only purchased-credit prestige is operated here
DIRECT_PAYMENT_ENABLED=false # per-run payment is outside the operated scope
CREDITS_ENABLED=true
@@ -74,7 +75,8 @@ service while the wallet/preflight and fleet activation gates remain open:
```bash
sudo systemctl restart wownerogue.service
journalctl -u wownerogue.service -n 50 --no-pager # verify: "Payment system: ENABLED", payouts disabled
curl -s localhost:3000/api/game-modes | jq '{freePlayEnabled, PAID_SINGLE, PAID_CREDITS}'
curl -fsS localhost:3000/api/game-modes \
| jq -e '.FREE.enabled == true and .PAID_CREDITS.enabled == true and (has("PAID_SINGLE") | not)'
```
**MAINNET WALLET NO-GO:** this application release does not authorize changing, copying, stopping,
@@ -150,6 +152,7 @@ DIFFICULTY_PRESET=casino
# --- payments + free play ---
PAYMENTS_ENABLED=true
GAME_MODE=PAID_SINGLE
PAYMENT_MODES=direct
FREE_PLAY_ENABLED=true # free and direct solo boards available
@@ -244,6 +247,9 @@ domains, websocket upgrades, and local readiness probes still work.
## Step 6 — Verify end to end
- `monerogue.app` loads; `play.wowne.ro` still healthy.
- Each public modes document contains only its operated paid solo product:
Wownero has `PAID_CREDITS` and no `PAID_SINGLE`; Monerogue has `PAID_SINGLE` and no
`PAID_CREDITS`.
- After wallet promotion and the separately approved bounded canary: buy credits / pay entry with a
stagenet wallet → game starts → qualifying solo outcome → 2×/3× test-coin payout arrives.
- After the corresponding gates clear, both boards populate: free game → Pleb, paid game → Hall of
+1
View File
@@ -39,6 +39,7 @@ PAID_ACKNOWLEDGEMENT_REQUIRED=true
# Free runs rank on the Pleb board; purchased credits rank on the paid prestige board.
PAYMENTS_ENABLED=true
FREE_PLAY_ENABLED=true
GAME_MODE=PAID_CREDITS
PAYMENT_MODES=credits
ALLOW_MIXED_MODE=false
PREFER_CREDITS_FIRST=true
+1
View File
@@ -40,6 +40,7 @@ PAID_ACKNOWLEDGEMENT_REQUIRED=true
# Purchased-credit entry is outside this operated profile and stays fail-closed.
PAYMENTS_ENABLED=true
FREE_PLAY_ENABLED=true
GAME_MODE=PAID_SINGLE
PAYMENT_MODES=direct
ALLOW_MIXED_MODE=false
PREFER_CREDITS_FIRST=false
+8 -2
View File
@@ -9,7 +9,10 @@
const { isSupported, familyFor } = require('../chain/chainProfile');
const { resolveMatchRuleset } = require('../game/rulesets');
const money = require('../money/atomic');
const { validateOperatedProductProfile } = require('./operatedProductProfiles');
const {
getOperatedProductProfile,
validateOperatedProductProfile
} = require('./operatedProductProfiles');
const {
PG_BIGINT_MAX,
cryptoRulesetSupported,
@@ -133,7 +136,10 @@ class EnvironmentValidator {
const financialSinkUrl = String(env.FINANCIAL_EVENT_SINK_URL || '').trim();
const financialSinkToken = String(env.FINANCIAL_EVENT_SINK_TOKEN || '').trim();
if (env.GAME_MODE && env.PAYMENT_MODES) {
const operatedProfile = getOperatedProductProfile(env);
const exactOperatedLegacyMode = operatedProfile
&& String(env.GAME_MODE || '').trim() === operatedProfile.gameMode;
if (env.GAME_MODE && env.PAYMENT_MODES && !exactOperatedLegacyMode) {
warnings.push('Both GAME_MODE and PAYMENT_MODES are set. PAYMENT_MODES takes precedence.');
}
+15
View File
@@ -28,6 +28,7 @@ const SOURCE_SOFTWARE = Object.freeze({
const PROFILES = Object.freeze({
[OPERATED_PRODUCT_PROFILE_IDS.WOW_PRESTIGE]: Object.freeze({
id: OPERATED_PRODUCT_PROFILE_IDS.WOW_PRESTIGE,
gameMode: 'PAID_CREDITS',
hostname: 'play.wowne.ro',
publicUrl: 'https://play.wowne.ro',
operatorName: 'Such Software',
@@ -41,6 +42,7 @@ const PROFILES = Object.freeze({
}),
[OPERATED_PRODUCT_PROFILE_IDS.XMR_STAGENET]: Object.freeze({
id: OPERATED_PRODUCT_PROFILE_IDS.XMR_STAGENET,
gameMode: 'PAID_SINGLE',
hostname: 'monerogue.app',
publicUrl: 'https://monerogue.app',
operatorName: 'Such Software',
@@ -73,6 +75,16 @@ function getOperatedProductProfile(env = process.env) {
return PROFILES[selectedProfileId(env)] || null;
}
function selectPublicPaidModeDescriptors(env = process.env, descriptors = {}) {
const profile = getOperatedProductProfile(env);
if (!profile) {
return { ...descriptors };
}
const descriptor = descriptors[profile.gameMode];
return descriptor === undefined ? {} : { [profile.gameMode]: descriptor };
}
function isExactPublicRoot(value, expected) {
try {
const actual = new URL(String(value || ''));
@@ -134,6 +146,8 @@ function validateOperatedProductProfile(env = process.env, config = {}) {
`TERMS_EFFECTIVE_DATE=${profile.termsEffectiveDate}`);
requireContract(isExplicitTrue(env.PAYMENTS_ENABLED) && config?.paymentsEnabled === true,
'PAYMENTS_ENABLED=true');
requireContract(String(env.GAME_MODE || '').trim() === profile.gameMode,
`GAME_MODE=${profile.gameMode}`);
requireContract(isExplicitTrue(env.PAID_ACKNOWLEDGEMENT_REQUIRED),
'PAID_ACKNOWLEDGEMENT_REQUIRED=true');
requireContract(isExplicitTrue(env.FREE_PLAY_ENABLED), 'FREE_PLAY_ENABLED=true');
@@ -190,6 +204,7 @@ module.exports = {
OPERATED_PRODUCT_PROFILE_IDS,
SOURCE_SOFTWARE,
getOperatedProductProfile,
selectPublicPaidModeDescriptors,
selectedProfileId,
validateOperatedProductProfile
};
+10 -3
View File
@@ -386,11 +386,18 @@ class PaymentConfigManager {
if (!this.config.paymentsEnabled) {
return 'FREE';
}
if (this.legacyOverride) {
return this.legacyOverride.toUpperCase();
}
const directEnabled = !!this.config.modes.direct.enabled;
const creditsEnabled = !!this.config.modes.credits.enabled;
const legacyOverride = String(this.legacyOverride || '').trim().toUpperCase();
// GAME_MODE is legacy compatibility input, not authority over the parsed payment
// policy. Honor it only when the selected mode is actually enabled; contradictory
// PAYMENT_MODES/per-mode switches must never be misreported to sockets or health.
if (legacyOverride === 'PAID_SINGLE' && directEnabled) {
return 'PAID_SINGLE';
}
if (legacyOverride === 'PAID_CREDITS' && creditsEnabled) {
return 'PAID_CREDITS';
}
if (directEnabled && !creditsEnabled) {
return 'PAID_SINGLE';
}
+21
View File
@@ -1258,6 +1258,27 @@ class GameModeManager {
};
}
getPublicPreferredPaymentMode(user) {
if (!this.paymentsEnabled) {
return 'free';
}
const preferred = String(user?.preferred_payment_mode || '').trim().toLowerCase();
if (preferred === 'direct' && this.directModeEnabled) {
return 'direct';
}
if (preferred === 'credits' && this.creditsModeEnabled) {
return 'credits';
}
if (this.creditsModeEnabled) {
return 'credits';
}
if (this.directModeEnabled) {
return 'direct';
}
return 'free';
}
/**
* Get available payment options for a user
* @param {string} socketId - Socket ID
+19 -14
View File
@@ -51,6 +51,7 @@ const { attachDungeonVerification } = require('./game/fairnessVerifier');
const { renderVerifyPage } = require('./views/verifyPage');
const { renderPrivacy, renderResponsiblePlay, renderTerms } = require('./views/legalPages');
const { buildCommerceDisclosure } = require('./config/commerceDisclosurePolicy');
const { selectPublicPaidModeDescriptors } = require('./config/operatedProductProfiles');
const createAdminRoutes = require('./routes/admin');
const createAuthRoutes = require('./routes/auth');
const { createLeaderboardHandler } = require('./routes/leaderboard');
@@ -399,7 +400,7 @@ app.get('/api/user/:socketId/mode', requireSessionOwnership(401), asyncHandler(a
}
res.json({
socketId,
preferredPaymentMode: user.preferred_payment_mode || 'direct',
preferredPaymentMode: gameModeManager.getPublicPreferredPaymentMode(user),
hasPayoutAddress: !!user.payout_address,
paymentsEnabled: gameModeManager.paymentsEnabled,
directModeEnabled: gameModeManager.directModeEnabled,
@@ -789,19 +790,7 @@ app.get('/api/game-modes', (req, res) => {
}
}
res.json({
operatedProductProfileId: disclosure.operatedProduct?.id || null,
cryptoMatchPayoutsEnabled: disclosure.service.cryptoMatchPayoutsEnabled,
soloEnabled: publicModeInfo.modes?.solo === true,
// Free play is available when the instance is free-only OR when free play is offered
// as a choice alongside paid options (FREE_PLAY_ENABLED).
freePlayEnabled: !!gameModeManager.freePlayEnabled,
FREE: {
name: 'Free Play',
cost: 0,
payoutMultiplier: 0,
enabled: !!gameModeManager.freePlayEnabled
},
const paidModeDescriptors = selectPublicPaidModeDescriptors(process.env, {
PAID_SINGLE: {
name: 'Paid Single Game',
cost: toDisplay(directMode.price),
@@ -818,7 +807,23 @@ app.get('/api/game-modes', (req, res) => {
payoutMultiplier: isPayoutProcessingEnabled() && creditsMode.enabled && config.payouts.rules.credits.enabled
? gameModeManager.getImplementedPayoutMultipliersForMode('PAID_CREDITS')
: 0
}
});
res.json({
operatedProductProfileId: disclosure.operatedProduct?.id || null,
cryptoMatchPayoutsEnabled: disclosure.service.cryptoMatchPayoutsEnabled,
soloEnabled: publicModeInfo.modes?.solo === true,
// Free play is available when the instance is free-only OR when free play is offered
// as a choice alongside paid options (FREE_PLAY_ENABLED).
freePlayEnabled: !!gameModeManager.freePlayEnabled,
FREE: {
name: 'Free Play',
cost: 0,
payoutMultiplier: 0,
enabled: !!gameModeManager.freePlayEnabled
},
...paidModeDescriptors,
match: {
enabled: publicModeInfo.modes?.match?.enabled === true,
economies: publicModeInfo.modes?.match?.economies || {}
+2 -3
View File
@@ -236,9 +236,8 @@ function assertPublicModeContract(modes) {
assert(modes?.soloEnabled === true && modes?.FREE?.enabled === true,
'the original solo mode and free solo entry must be enabled');
assert(modes?.PAID_SINGLE?.enabled === true
&& modes?.PAID_CREDITS?.enabled === false
&& Number(modes?.PAID_CREDITS?.payoutMultiplier) === 0,
'direct solo must be enabled and purchased-credit solo must be disabled');
&& !Object.prototype.hasOwnProperty.call(modes || {}, 'PAID_CREDITS'),
'direct solo must be enabled and purchased-credit solo must be omitted');
assert(modes?.match?.enabled === true
&& JSON.stringify(enabledEconomyIds(modes?.match?.economies))
=== JSON.stringify(['credits_prestige', 'free']),
+15
View File
@@ -192,6 +192,7 @@ describe('EnvironmentValidator production launch gate', () => {
CRYPTO_TYPE: 'WOW',
MONERO_NETWORK: 'mainnet',
FREE_PLAY_ENABLED: 'true',
GAME_MODE: 'PAID_CREDITS',
PAYMENT_MODES: 'credits',
DIRECT_PAYMENT_ENABLED: 'false',
CREDITS_ENABLED: 'true',
@@ -219,6 +220,16 @@ describe('EnvironmentValidator production launch gate', () => {
expect(new EnvironmentValidator({ env, logger: silentLogger })
.assertValid(operatedConfig).errors).toEqual([]);
for (const gameMode of [undefined, 'PAID_SINGLE']) {
const driftedEnv = { ...env };
if (gameMode === undefined) delete driftedEnv.GAME_MODE;
else driftedEnv.GAME_MODE = gameMode;
expect(new EnvironmentValidator({ env: driftedEnv, logger: silentLogger })
.validate(operatedConfig).errors).toContain(
'OPERATED_PRODUCT_PROFILE=such-play-wow-prestige requires GAME_MODE=PAID_CREDITS.'
);
}
expect(new EnvironmentValidator({ env, logger: silentLogger }).validate({
...operatedConfig,
products: { cosmetic: [{ id: 'unrelated-pack', price: 1n }] }
@@ -237,6 +248,7 @@ describe('EnvironmentValidator production launch gate', () => {
LEGAL_POLICY_VERSION: '2026-07-23-v2',
TERMS_EFFECTIVE_DATE: '2026-07-23',
FREE_PLAY_ENABLED: 'true',
GAME_MODE: 'PAID_SINGLE',
PAYMENT_MODES: 'direct',
DIRECT_PAYMENT_ENABLED: 'true',
CREDITS_ENABLED: 'false',
@@ -312,6 +324,7 @@ describe('EnvironmentValidator production launch gate', () => {
CRYPTO_TYPE: 'WOW',
MONERO_NETWORK: 'mainnet',
FREE_PLAY_ENABLED: 'true',
GAME_MODE: 'PAID_CREDITS',
PAYMENT_MODES: 'direct,credits',
DIRECT_PAYMENT_ENABLED: 'true',
CREDITS_ENABLED: 'true',
@@ -345,6 +358,7 @@ describe('EnvironmentValidator production launch gate', () => {
CRYPTO_TYPE: 'XMR',
MONERO_NETWORK: 'mainnet',
FREE_PLAY_ENABLED: 'true',
GAME_MODE: 'PAID_CREDITS',
PAYMENT_MODES: 'direct,credits',
DIRECT_PAYMENT_ENABLED: 'true',
CREDITS_ENABLED: 'true',
@@ -376,6 +390,7 @@ describe('EnvironmentValidator production launch gate', () => {
expect.stringContaining('requires LEGAL_POLICY_VERSION=2026-07-23-v2'),
expect.stringContaining('requires TERMS_EFFECTIVE_DATE=2026-07-23'),
expect.stringContaining('requires MONERO_NETWORK=stagenet'),
expect.stringContaining('requires GAME_MODE=PAID_SINGLE'),
expect.stringContaining('requires PAYMENT_MODES=direct'),
expect.stringContaining('requires CREDITS_ENABLED=false'),
expect.stringContaining('requires CREDITS_PAYOUTS_ENABLED=false'),
+2
View File
@@ -47,6 +47,7 @@ describe('operated product environment templates', () => {
CRYPTO_TYPE: 'WOW',
MONERO_NETWORK: 'mainnet',
FREE_PLAY_ENABLED: 'true',
GAME_MODE: 'PAID_CREDITS',
PAYMENT_MODES: 'credits',
DIRECT_PAYMENT_ENABLED: 'false',
CREDITS_ENABLED: 'true',
@@ -78,6 +79,7 @@ describe('operated product environment templates', () => {
CRYPTO_TYPE: 'XMR',
MONERO_NETWORK: 'stagenet',
FREE_PLAY_ENABLED: 'true',
GAME_MODE: 'PAID_SINGLE',
PAYMENT_MODES: 'direct',
DIRECT_PAYMENT_ENABLED: 'true',
CREDITS_ENABLED: 'false',
+35
View File
@@ -0,0 +1,35 @@
const {
selectPublicPaidModeDescriptors
} = require('../src/config/operatedProductProfiles');
const descriptors = Object.freeze({
PAID_SINGLE: Object.freeze({ enabled: true, name: 'Paid Single Game' }),
PAID_CREDITS: Object.freeze({ enabled: true, name: 'Credits Package' })
});
describe('operated-product public paid-mode contract', () => {
test('play.wowne.ro publishes credits and omits direct entry', () => {
const result = selectPublicPaidModeDescriptors({
OPERATED_PRODUCT_PROFILE: 'such-play-wow-prestige'
}, descriptors);
expect(result).toEqual({ PAID_CREDITS: descriptors.PAID_CREDITS });
expect(result).not.toHaveProperty('PAID_SINGLE');
});
test('monerogue.app publishes direct entry and omits purchased credits', () => {
const result = selectPublicPaidModeDescriptors({
OPERATED_PRODUCT_PROFILE: 'such-monerogue-stagenet'
}, descriptors);
expect(result).toEqual({ PAID_SINGLE: descriptors.PAID_SINGLE });
expect(result).not.toHaveProperty('PAID_CREDITS');
});
test('generic MIT deployments retain both legacy API descriptors', () => {
const result = selectPublicPaidModeDescriptors({}, descriptors);
expect(result).toEqual(descriptors);
expect(result).not.toBe(descriptors);
});
});
+32
View File
@@ -105,6 +105,38 @@ describe('production payment policy wiring', () => {
expect(wallet.createPaymentRequest).not.toHaveBeenCalled();
});
test('a contradictory legacy GAME_MODE cannot misreport the enabled payment policy', () => {
process.env.PAYMENTS_ENABLED = 'true';
process.env.GAME_MODE = 'PAID_SINGLE';
process.env.PAYMENT_MODES = 'credits';
process.env.DIRECT_PAYMENT_ENABLED = 'false';
process.env.CREDITS_ENABLED = 'true';
process.env.PAYOUTS_ENABLED = 'false';
process.env.DIRECT_PAYOUTS_ENABLED = 'false';
process.env.CREDITS_PAYOUTS_ENABLED = 'false';
const manager = new PaymentConfigManager({ logger: { warn() {}, info() {} } });
const gameModes = new GameModeManager(
{ query: jest.fn(), withTransaction: jest.fn() },
{ validateAddress: jest.fn() },
{ CONSOLE_LOGGING: false },
manager
);
expect(manager.getConfig().modes.direct.enabled).toBe(false);
expect(manager.getConfig().modes.credits.enabled).toBe(true);
expect(manager.getLegacyGameMode()).toBe('PAID_CREDITS');
expect(gameModes.getGameModeInfo()).toEqual(expect.objectContaining({
mode: 'PAID_CREDITS',
directModeEnabled: false,
creditsModeEnabled: true,
payoutsEnabled: false
}));
expect(gameModes.getPublicPreferredPaymentMode({
preferred_payment_mode: 'direct'
})).toBe('credits');
});
test('global per-game payout cap is an outer bound for direct and credit wins', () => {
process.env.PAYMENT_MODES = 'direct,credits';
process.env.PAYOUT_MAX_PER_GAME = '123456';
-4
View File
@@ -185,10 +185,6 @@ describe('XMR stagenet financial canary safety contract', () => {
enabled: true,
payoutMultiplier: { escape: 2, escapeWithTreasure: 3 }
},
PAID_CREDITS: {
enabled: false,
payoutMultiplier: 0
},
match: {
enabled: true,
economies: {