mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
875 lines
36 KiB
JavaScript
875 lines
36 KiB
JavaScript
const EnvironmentValidator = require('../src/config/environmentValidator');
|
|
|
|
function config(overrides = {}) {
|
|
const base = {
|
|
paymentsEnabled: true,
|
|
currency: { symbol: 'XMR' },
|
|
modes: {
|
|
direct: { enabled: true, price: 10000n },
|
|
credits: {
|
|
enabled: true,
|
|
packages: [{ id: 'small', credits: 10, price: 90000000000n }]
|
|
}
|
|
},
|
|
payouts: {
|
|
enabled: false,
|
|
rules: {
|
|
direct: {
|
|
enabled: false,
|
|
multipliers: { escape: 2, escapeWithTreasure: 3 },
|
|
minPayout: 1n,
|
|
maxPayout: 50000n
|
|
},
|
|
credits: {
|
|
enabled: false,
|
|
multipliers: { escape: 2, escapeWithTreasure: 3 },
|
|
baseValue: 10000n,
|
|
minPayout: 1n,
|
|
maxPayout: 50000n
|
|
}
|
|
}
|
|
}
|
|
};
|
|
return {
|
|
...base,
|
|
...overrides,
|
|
modes: {
|
|
...base.modes,
|
|
...(overrides.modes || {})
|
|
},
|
|
payouts: {
|
|
...base.payouts,
|
|
...(overrides.payouts || {}),
|
|
rules: {
|
|
direct: {
|
|
...base.payouts.rules.direct,
|
|
...(overrides.payouts?.rules?.direct || {})
|
|
},
|
|
credits: {
|
|
...base.payouts.rules.credits,
|
|
...(overrides.payouts?.rules?.credits || {})
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function productionEnv(overrides = {}) {
|
|
return {
|
|
NODE_ENV: 'production',
|
|
CRYPTO_TYPE: 'XMR',
|
|
MONERO_NETWORK: 'stagenet',
|
|
PAYMENTS_ENABLED: 'true',
|
|
PAYOUTS_ENABLED: 'false',
|
|
DIRECT_PAYOUTS_ENABLED: 'false',
|
|
CREDITS_PAYOUTS_ENABLED: 'false',
|
|
PRIMARY_WALLET_ENDPOINT: 'http://127.0.0.1:38083',
|
|
WALLET_RPC_USER: 'monerogue',
|
|
WALLET_RPC_PASSWORD: 'wallet-rpc-correct-horse-2026',
|
|
PRIMARY_RPC_ENDPOINT: 'http://127.0.0.1:38081',
|
|
DB_PASSWORD: 'correct-horse-battery-staple-2026',
|
|
ADMIN_API_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
|
OPERATOR_NAME: 'Test Operator LLC',
|
|
OPERATOR_CONTACT_URL: 'mailto:support@operator.invalid',
|
|
LEGAL_POLICY_VERSION: '2026-07-21-v1',
|
|
TERMS_EFFECTIVE_DATE: '2026-07-21',
|
|
MINIMUM_AGE: '18',
|
|
PAID_ACKNOWLEDGEMENT_REQUIRED: 'true',
|
|
BLOCK_SOURCE: 'daemon',
|
|
TRUST_PROXY: 'true',
|
|
MATCH_PAID_ENTROPY_DELAY_BLOCKS: '2',
|
|
MATCH_PAID_ENTROPY_CONFIRMATIONS: '2',
|
|
PAYOUT_MAX_PER_GAME: '50000',
|
|
BALANCE_CRITICAL: '50000',
|
|
BALANCE_WARN: '100000',
|
|
CREDITS_PACKAGES: '[{"id":"small","credits":10,"price":"90000000000"}]',
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
const silentLogger = {
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn()
|
|
};
|
|
|
|
describe('EnvironmentValidator production launch gate', () => {
|
|
beforeEach(() => jest.clearAllMocks());
|
|
|
|
test('accepts explicit paid-credits plus free-play style config with payouts disabled', () => {
|
|
const validator = new EnvironmentValidator({ env: productionEnv(), logger: silentLogger });
|
|
const result = validator.assertValid(config());
|
|
|
|
expect(result.errors).toEqual([]);
|
|
expect(result.money).toEqual(expect.objectContaining({
|
|
paymentsEnabled: true,
|
|
payoutsEnabled: false
|
|
}));
|
|
});
|
|
|
|
test('accepts a complete HTTPS mainnet financial-event sink without exposing its token', () => {
|
|
const token = 'A1b2C3d4E5f6G7h8J9k0L1m2N3p4Q5r6';
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({
|
|
MONERO_NETWORK: 'mainnet',
|
|
FINANCIAL_EVENT_SINK_URL: 'https://ledger.operator.invalid/v1/events',
|
|
FINANCIAL_EVENT_SINK_TOKEN: token,
|
|
FINANCIAL_EVENT_ACCOUNT_REF: 'wowngeon:receipts'
|
|
}),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
|
|
expect(result.errors).toEqual([]);
|
|
expect(JSON.stringify(result)).not.toContain(token);
|
|
});
|
|
|
|
test('rejects partial, insecure, weak, credential-bearing, or testnet accounting export', () => {
|
|
const token = 'A1b2C3d4E5f6G7h8J9k0L1m2N3p4Q5r6';
|
|
const cases = [
|
|
[
|
|
{ FINANCIAL_EVENT_SINK_URL: 'https://ledger.operator.invalid/events' },
|
|
'must be configured together'
|
|
],
|
|
[
|
|
{
|
|
FINANCIAL_EVENT_SINK_URL: 'http://ledger.operator.invalid/events',
|
|
FINANCIAL_EVENT_SINK_TOKEN: token,
|
|
MONERO_NETWORK: 'mainnet'
|
|
},
|
|
'must use https:// in production'
|
|
],
|
|
[
|
|
{
|
|
FINANCIAL_EVENT_SINK_URL: 'https://user:pass@ledger.operator.invalid/events',
|
|
FINANCIAL_EVENT_SINK_TOKEN: token,
|
|
MONERO_NETWORK: 'mainnet'
|
|
},
|
|
'must not embed credentials'
|
|
],
|
|
[
|
|
{
|
|
FINANCIAL_EVENT_SINK_URL: 'https://ledger.operator.invalid/events',
|
|
FINANCIAL_EVENT_SINK_TOKEN: 'change-me'
|
|
},
|
|
'strong non-placeholder secret'
|
|
],
|
|
[
|
|
{
|
|
FINANCIAL_EVENT_SINK_URL: 'https://ledger.operator.invalid/events',
|
|
FINANCIAL_EVENT_SINK_TOKEN: token
|
|
},
|
|
'must be unset on non-mainnet networks'
|
|
],
|
|
[
|
|
{
|
|
FINANCIAL_EVENT_SINK_URL: 'https://ledger.operator.invalid/events',
|
|
FINANCIAL_EVENT_SINK_TOKEN: token,
|
|
FINANCIAL_EVENT_ACCOUNT_REF: 'customer@example.invalid',
|
|
MONERO_NETWORK: 'mainnet'
|
|
},
|
|
'must be a non-PII identifier'
|
|
]
|
|
];
|
|
|
|
for (const [overrides, expected] of cases) {
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv(overrides), logger: silentLogger
|
|
}).validate(config());
|
|
expect(result.errors).toEqual(expect.arrayContaining([expect.stringContaining(expected)]));
|
|
expect(JSON.stringify(result)).not.toContain(token);
|
|
}
|
|
});
|
|
|
|
test('accepts the exact Such Software play.wowne.ro operated-product contract', () => {
|
|
const env = productionEnv({
|
|
OPERATED_PRODUCT_PROFILE: 'such-play-wow-prestige',
|
|
HOSTED_BY: 'https://play.wowne.ro',
|
|
OPERATOR_NAME: 'Such Software',
|
|
OPERATOR_CONTACT_URL: 'mailto:apps@such.software',
|
|
OPERATOR_CONTACT_LABEL: 'apps@such.software',
|
|
LEGAL_POLICY_VERSION: '2026-07-22-v1',
|
|
TERMS_EFFECTIVE_DATE: '2026-07-22',
|
|
CRYPTO_TYPE: 'WOW',
|
|
MONERO_NETWORK: 'mainnet',
|
|
FREE_PLAY_ENABLED: 'true',
|
|
GAME_MODE: 'PAID_CREDITS',
|
|
PAYMENT_MODES: 'credits',
|
|
DIRECT_PAYMENT_ENABLED: 'false',
|
|
CREDITS_ENABLED: 'true',
|
|
PAYOUTS_ENABLED: 'false',
|
|
DIRECT_PAYOUTS_ENABLED: 'false',
|
|
CREDITS_PAYOUTS_ENABLED: 'false',
|
|
ALLOW_MAINNET_PAYOUTS: 'false',
|
|
SOLO_ENABLED: 'true',
|
|
TAVERN_ENABLED: 'true',
|
|
MATCH_ENABLED: 'true',
|
|
MATCH_CRYPTO_RACE_ENABLED: 'false',
|
|
MATCH_PAYOUTS_ENABLED: 'false'
|
|
});
|
|
const operatedConfig = config({
|
|
currency: { symbol: 'WOW' },
|
|
modes: {
|
|
direct: { enabled: false, price: 10000n },
|
|
credits: {
|
|
enabled: true,
|
|
packages: [{ id: 'small', credits: 10, price: 90000000000n }]
|
|
}
|
|
}
|
|
});
|
|
|
|
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 }] }
|
|
}).errors).toContain(
|
|
'OPERATED_PRODUCT_PROFILE=such-play-wow-prestige requires no standalone products or bundled entitlement grants; credit packages may grant only their top-level credits and bonus.'
|
|
);
|
|
});
|
|
|
|
test('accepts the exact direct-only Such Software monerogue.app stagenet 2x/3x contract', () => {
|
|
const env = productionEnv({
|
|
OPERATED_PRODUCT_PROFILE: 'such-monerogue-stagenet',
|
|
HOSTED_BY: 'https://monerogue.app',
|
|
OPERATOR_NAME: 'Such Software',
|
|
OPERATOR_CONTACT_URL: 'mailto:apps@such.software',
|
|
OPERATOR_CONTACT_LABEL: 'apps@such.software',
|
|
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',
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUTS_ENABLED: 'true',
|
|
CREDITS_PAYOUTS_ENABLED: 'false',
|
|
DIRECT_PAYOUT_ESCAPE: '2.0',
|
|
DIRECT_PAYOUT_TREASURE: '3.0',
|
|
ALLOW_MAINNET_PAYOUTS: 'false',
|
|
SOLO_ENABLED: 'true',
|
|
TAVERN_ENABLED: 'true',
|
|
MATCH_ENABLED: 'true',
|
|
MATCH_CRYPTO_RACE_ENABLED: 'false',
|
|
MATCH_PAYOUTS_ENABLED: 'false'
|
|
});
|
|
const operatedConfig = config({
|
|
modes: {
|
|
direct: { enabled: true, price: 10000n },
|
|
credits: {
|
|
enabled: false,
|
|
packages: [{ id: 'small', credits: 10, price: 90000000000n }]
|
|
}
|
|
},
|
|
payouts: {
|
|
enabled: true,
|
|
rules: {
|
|
direct: { enabled: true },
|
|
credits: { enabled: false }
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(new EnvironmentValidator({ env, logger: silentLogger })
|
|
.assertValid(operatedConfig).errors).toEqual([]);
|
|
|
|
const outOfScopeCatalogs = [
|
|
{
|
|
...operatedConfig,
|
|
products: { cosmetic: [{ id: 'pack', price: 20000n }] }
|
|
},
|
|
...[
|
|
{ packs: ['kenney-3d-characters'] },
|
|
{ premiumLevel: 'supporter' },
|
|
{ race_entries: 2, race_entry_value_atomic: '10000' },
|
|
{ future_entitlement: true },
|
|
{ credits: 10 }
|
|
].map(grants => ({
|
|
...operatedConfig,
|
|
modes: {
|
|
...operatedConfig.modes,
|
|
credits: {
|
|
...operatedConfig.modes.credits,
|
|
packages: [{ id: 'small', credits: 10, price: 90000000000n, grants }]
|
|
}
|
|
}
|
|
}))
|
|
];
|
|
for (const widenedCatalog of outOfScopeCatalogs) {
|
|
expect(new EnvironmentValidator({ env, logger: silentLogger })
|
|
.validate(widenedCatalog).errors).toContain(
|
|
'OPERATED_PRODUCT_PROFILE=such-monerogue-stagenet requires no standalone products or bundled entitlement grants; credit packages may grant only their top-level credits and bonus.'
|
|
);
|
|
}
|
|
});
|
|
|
|
test('play.wowne.ro profile rejects direct entry or any payout activation', () => {
|
|
const env = productionEnv({
|
|
OPERATED_PRODUCT_PROFILE: 'such-play-wow-prestige',
|
|
HOSTED_BY: 'https://play.wowne.ro',
|
|
OPERATOR_NAME: 'Such Software',
|
|
OPERATOR_CONTACT_URL: 'mailto:apps@such.software',
|
|
OPERATOR_CONTACT_LABEL: 'apps@such.software',
|
|
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',
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUTS_ENABLED: 'true',
|
|
CREDITS_PAYOUTS_ENABLED: 'false',
|
|
ALLOW_MAINNET_PAYOUTS: 'false',
|
|
MATCH_CRYPTO_RACE_ENABLED: 'false',
|
|
MATCH_PAYOUTS_ENABLED: 'false'
|
|
});
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config({
|
|
currency: { symbol: 'WOW' },
|
|
payouts: { enabled: true, rules: { direct: { enabled: true } } }
|
|
}));
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('requires PAYMENT_MODES=credits'),
|
|
expect.stringContaining('requires DIRECT_PAYMENT_ENABLED=false'),
|
|
expect.stringContaining('requires PAYOUTS_ENABLED=false'),
|
|
expect.stringContaining('requires DIRECT_PAYOUTS_ENABLED=false')
|
|
]));
|
|
});
|
|
|
|
test('operated profiles fail closed on identity, network, mode, multiplier, or payout drift', () => {
|
|
const env = productionEnv({
|
|
OPERATED_PRODUCT_PROFILE: 'such-monerogue-stagenet',
|
|
HOSTED_BY: 'https://play.wowne.ro',
|
|
OPERATOR_NAME: 'Someone Else',
|
|
OPERATOR_CONTACT_URL: 'mailto:other@example.invalid',
|
|
OPERATOR_CONTACT_LABEL: 'Other',
|
|
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',
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUTS_ENABLED: 'true',
|
|
CREDITS_PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUT_ESCAPE: '4',
|
|
DIRECT_PAYOUT_TREASURE: '3',
|
|
CREDITS_PAYOUT_ESCAPE: '2',
|
|
CREDITS_PAYOUT_TREASURE: '3',
|
|
ALLOW_MAINNET_PAYOUTS: 'true',
|
|
SOLO_ENABLED: 'false',
|
|
TAVERN_ENABLED: 'false',
|
|
MATCH_ENABLED: 'false',
|
|
MATCH_CRYPTO_RACE_ENABLED: 'true',
|
|
MATCH_PAYOUTS_ENABLED: 'true'
|
|
});
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config({
|
|
payouts: {
|
|
enabled: true,
|
|
rules: { direct: { enabled: true }, credits: { enabled: true } }
|
|
}
|
|
}));
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('requires HOSTED_BY=https://monerogue.app'),
|
|
expect.stringContaining('requires OPERATOR_NAME=Such Software'),
|
|
expect.stringContaining('requires OPERATOR_CONTACT_URL=mailto:apps@such.software'),
|
|
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'),
|
|
expect.stringContaining('requires DIRECT_PAYOUT_ESCAPE=2'),
|
|
expect.stringContaining('requires SOLO_ENABLED=true'),
|
|
expect.stringContaining('requires TAVERN_ENABLED=true'),
|
|
expect.stringContaining('requires MATCH_ENABLED=true'),
|
|
expect.stringContaining('requires MATCH_CRYPTO_RACE_ENABLED=false'),
|
|
expect.stringContaining('requires MATCH_PAYOUTS_ENABLED=false'),
|
|
expect.stringContaining('requires ALLOW_MAINNET_PAYOUTS=false')
|
|
]));
|
|
});
|
|
|
|
test('unknown operated profile is rejected while an omitted profile preserves generic MIT hosting', () => {
|
|
expect(new EnvironmentValidator({ env: productionEnv(), logger: silentLogger })
|
|
.validate(config()).errors).toEqual([]);
|
|
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({ OPERATED_PRODUCT_PROFILE: 'third-party-claim' }),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
expect(result.errors).toContain(
|
|
'OPERATED_PRODUCT_PROFILE=third-party-claim is unknown. Remove it for an independent MIT deployment or use a reviewed Such Software profile.'
|
|
);
|
|
});
|
|
|
|
test('operated identity cannot bypass its exact contract outside production', () => {
|
|
const env = productionEnv({
|
|
NODE_ENV: 'development',
|
|
OPERATED_PRODUCT_PROFILE: 'such-play-wow-prestige',
|
|
HOSTED_BY: 'http://localhost:3000',
|
|
OPERATOR_NAME: 'Such Software'
|
|
});
|
|
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config());
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('requires HOSTED_BY=https://play.wowne.ro'),
|
|
expect.stringContaining('requires CRYPTO_TYPE=WOW'),
|
|
expect.stringContaining('requires PAYMENT_MODES=credits')
|
|
]));
|
|
});
|
|
|
|
test('accepts explicitly capped stagenet crypto match payouts', () => {
|
|
const env = productionEnv({
|
|
PAYOUTS_ENABLED: 'true',
|
|
MATCH_ENABLED: 'true',
|
|
MATCH_CRYPTO_RACE_ENABLED: 'true',
|
|
MATCH_PAYOUTS_ENABLED: 'true',
|
|
MATCH_PAYOUT_MAX: '50000',
|
|
MATCH_ENTRY_FEE_ATOMIC: '10000',
|
|
MATCH_HOUSE_FEE_PERCENT: '5',
|
|
MATCH_RULESET_ID: 'race',
|
|
MATCH_MAX_PLAYERS: '4'
|
|
});
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger })
|
|
.assertValid(config({
|
|
payouts: { enabled: true },
|
|
products: {
|
|
cosmetic: [{
|
|
id: 'race_2',
|
|
price: 20000n,
|
|
grants: { race_entries: 2, race_entry_value_atomic: '10000' }
|
|
}]
|
|
}
|
|
}));
|
|
|
|
expect(result.money.matchPayoutsEnabled).toBe(true);
|
|
});
|
|
|
|
test.each([
|
|
['MATCH_PAID_ENTROPY_DELAY_BLOCKS', '1'],
|
|
['MATCH_PAID_ENTROPY_CONFIRMATIONS', '1'],
|
|
['MATCH_PAID_ENTROPY_DELAY_BLOCKS', '2.5'],
|
|
['MATCH_PAID_ENTROPY_CONFIRMATIONS', '9007199254740992'],
|
|
['MATCH_PAID_ENTROPY_DELAY_BLOCKS', '101']
|
|
])('rejects unsafe production paid entropy setting %s=%s', (key, value) => {
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({ MATCH_ENABLED: 'true', [key]: value }),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
|
|
expect(result.errors).toContain(
|
|
`${key} must be an explicit safe integer from 2 through 100 when production match mode is enabled.`
|
|
);
|
|
});
|
|
|
|
test('rejects crypto co-op and a cap below the configured maximum pot liability', () => {
|
|
const env = productionEnv({
|
|
PAYOUTS_ENABLED: 'true',
|
|
MATCH_ENABLED: 'true',
|
|
MATCH_CRYPTO_RACE_ENABLED: 'true',
|
|
MATCH_PAYOUTS_ENABLED: 'true',
|
|
MATCH_PAYOUT_MAX: '100',
|
|
MATCH_ENTRY_FEE_ATOMIC: '10000',
|
|
MATCH_HOUSE_FEE_PERCENT: '5',
|
|
MATCH_RULESET_ID: 'coop-escape',
|
|
MATCH_MAX_PLAYERS: '4'
|
|
});
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger })
|
|
.validate(config({ payouts: { enabled: true } }));
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('single-winner crypto payout semantics'),
|
|
expect.stringContaining('below the configured 4-player winner liability')
|
|
]));
|
|
});
|
|
|
|
test('rejects mismatched or underfunded race-ticket products', () => {
|
|
const env = productionEnv({
|
|
PAYOUTS_ENABLED: 'true',
|
|
MATCH_ENABLED: 'true',
|
|
MATCH_CRYPTO_RACE_ENABLED: 'true',
|
|
MATCH_PAYOUTS_ENABLED: 'true',
|
|
MATCH_PAYOUT_MAX: '50000',
|
|
MATCH_ENTRY_FEE_ATOMIC: '10000',
|
|
MATCH_HOUSE_FEE_PERCENT: '5',
|
|
MATCH_RULESET_ID: 'race',
|
|
MATCH_MAX_PLAYERS: '4'
|
|
});
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config({
|
|
payouts: { enabled: true },
|
|
products: {
|
|
cosmetic: [
|
|
{
|
|
id: 'wrong-value',
|
|
price: 20000n,
|
|
grants: { race_entries: 2, race_entry_value_atomic: '9000' }
|
|
},
|
|
{
|
|
id: 'underfunded',
|
|
price: 19999n,
|
|
grants: { race_entries: 2, race_entry_value_atomic: '10000' }
|
|
}
|
|
]
|
|
}
|
|
}));
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('wrong-value backing must equal MATCH_ENTRY_FEE_ATOMIC'),
|
|
expect.stringContaining('underfunded price must fund at least 20000')
|
|
]));
|
|
});
|
|
|
|
test('rejects a 100% or over-precision match house fee', () => {
|
|
for (const value of ['100', '5.555']) {
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({ MATCH_HOUSE_FEE_PERCENT: value }),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
expect(result.errors).toContain(
|
|
'MATCH_HOUSE_FEE_PERCENT must be at least 0 and less than 100.'
|
|
);
|
|
}
|
|
});
|
|
|
|
test('missing production secrets and RPC endpoints are fatal', () => {
|
|
const env = productionEnv({
|
|
ADMIN_API_KEY: 'change-me',
|
|
DB_PASSWORD: '',
|
|
PRIMARY_WALLET_ENDPOINT: '',
|
|
PRIMARY_RPC_ENDPOINT: ''
|
|
});
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config());
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('PRIMARY_WALLET_ENDPOINT'),
|
|
expect.stringContaining('PRIMARY_RPC_ENDPOINT'),
|
|
expect.stringContaining('ADMIN_API_KEY'),
|
|
expect.stringContaining('DB_PASSWORD')
|
|
]));
|
|
});
|
|
|
|
test('wallet RPC authentication requires a complete credential pair', () => {
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({ WALLET_RPC_USER: 'rpc-user', WALLET_RPC_PASSWORD: '' }),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
|
|
expect(result.errors).toContain('WALLET_RPC_USER and WALLET_RPC_PASSWORD must be set together.');
|
|
});
|
|
|
|
test('production payments reject unauthenticated or remotely exposed wallet RPC', () => {
|
|
const missing = productionEnv({ WALLET_RPC_USER: '', WALLET_RPC_PASSWORD: '' });
|
|
const missingResult = new EnvironmentValidator({ env: missing, logger: silentLogger }).validate(config());
|
|
expect(missingResult.errors).toContain(
|
|
'Production financial workers require WALLET_RPC_USER and WALLET_RPC_PASSWORD for wallet-rpc Digest authentication.'
|
|
);
|
|
|
|
const remote = productionEnv({ PRIMARY_WALLET_ENDPOINT: 'http://192.0.2.20:38083' });
|
|
const remoteResult = new EnvironmentValidator({ env: remote, logger: silentLogger }).validate(config());
|
|
expect(remoteResult.errors).toContain(
|
|
'Production wallet-rpc over plaintext HTTP must bind to a loopback address.'
|
|
);
|
|
});
|
|
|
|
test('payout-only recovery still requires an authenticated loopback wallet', () => {
|
|
const payoutOnlyConfig = config({
|
|
paymentsEnabled: false,
|
|
modes: {
|
|
direct: { enabled: false },
|
|
credits: { enabled: false, packages: [] }
|
|
},
|
|
payouts: { enabled: true }
|
|
});
|
|
const env = productionEnv({
|
|
PAYMENTS_ENABLED: 'false',
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYMENT_ENABLED: 'false',
|
|
CREDITS_ENABLED: 'false',
|
|
MONERO_NETWORK: 'mainnet',
|
|
WALLET_RPC_USER: '',
|
|
WALLET_RPC_PASSWORD: '',
|
|
PRIMARY_WALLET_ENDPOINT: 'http://192.0.2.20:38083'
|
|
});
|
|
|
|
const errors = new EnvironmentValidator({ env, logger: silentLogger })
|
|
.validate(payoutOnlyConfig).errors;
|
|
|
|
expect(errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('Production financial workers require WALLET_RPC_USER'),
|
|
expect.stringContaining('plaintext HTTP must bind to a loopback'),
|
|
expect.stringContaining('ALLOW_MAINNET_PAYOUTS=true')
|
|
]));
|
|
|
|
env.PRIMARY_WALLET_ENDPOINT = '';
|
|
expect(new EnvironmentValidator({ env, logger: silentLogger })
|
|
.validate(payoutOnlyConfig).errors)
|
|
.toEqual(expect.arrayContaining([
|
|
expect.stringContaining('Financial workers require a wallet'),
|
|
expect.stringContaining('PRIMARY_WALLET_ENDPOINT')
|
|
]));
|
|
});
|
|
|
|
test('payout-only recovery cannot bypass the mainnet acknowledgement', () => {
|
|
const payoutOnlyConfig = config({
|
|
paymentsEnabled: false,
|
|
modes: {
|
|
direct: { enabled: false },
|
|
credits: { enabled: false, packages: [] }
|
|
},
|
|
payouts: { enabled: true }
|
|
});
|
|
const env = productionEnv({
|
|
PAYMENTS_ENABLED: 'false',
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYMENT_ENABLED: 'false',
|
|
CREDITS_ENABLED: 'false',
|
|
MONERO_NETWORK: 'mainnet'
|
|
});
|
|
|
|
const validator = new EnvironmentValidator({ env, logger: silentLogger });
|
|
expect(validator.validate(payoutOnlyConfig).errors).toContain(
|
|
'Mainnet payouts require the explicit safety acknowledgement ALLOW_MAINNET_PAYOUTS=true.'
|
|
);
|
|
env.ALLOW_MAINNET_PAYOUTS = 'true';
|
|
expect(validator.validate(payoutOnlyConfig).errors).toEqual([]);
|
|
});
|
|
|
|
test('production requires explicit payout intent instead of dangerous defaults', () => {
|
|
const env = productionEnv();
|
|
delete env.PAYOUTS_ENABLED;
|
|
delete env.DIRECT_PAYOUTS_ENABLED;
|
|
delete env.CREDITS_PAYOUTS_ENABLED;
|
|
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config());
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('explicit PAYOUTS_ENABLED'),
|
|
expect.stringContaining('explicit DIRECT_PAYOUTS_ENABLED'),
|
|
expect.stringContaining('explicit CREDITS_PAYOUTS_ENABLED')
|
|
]));
|
|
});
|
|
|
|
test('production paid play requires truthful operator metadata and acknowledgement policy', () => {
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({
|
|
OPERATOR_NAME: 'CHANGE_ME',
|
|
OPERATOR_CONTACT_URL: 'javascript:alert(1)',
|
|
LEGAL_POLICY_VERSION: '',
|
|
TERMS_EFFECTIVE_DATE: '2026-02-30',
|
|
MINIMUM_AGE: '0',
|
|
PAID_ACKNOWLEDGEMENT_REQUIRED: 'false'
|
|
}),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('PAID_ACKNOWLEDGEMENT_REQUIRED=true'),
|
|
expect.stringContaining('LEGAL_POLICY_VERSION'),
|
|
expect.stringContaining('TERMS_EFFECTIVE_DATE'),
|
|
expect.stringContaining('MINIMUM_AGE'),
|
|
expect.stringContaining('OPERATOR_NAME'),
|
|
expect.stringContaining('OPERATOR_CONTACT_URL')
|
|
]));
|
|
});
|
|
|
|
test('rejects misspelled booleans and legacy modes instead of using defaults', () => {
|
|
const env = productionEnv({
|
|
GAME_MODE: 'FRE',
|
|
PAYOUTS_ENABLED: 'flase',
|
|
DIRECT_PAYOUTS_ENABLED: 'treu'
|
|
});
|
|
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config());
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('GAME_MODE must be'),
|
|
expect.stringContaining('PAYOUTS_ENABLED must be an explicit boolean'),
|
|
expect.stringContaining('DIRECT_PAYOUTS_ENABLED must be an explicit boolean')
|
|
]));
|
|
});
|
|
|
|
test('rejects malformed atomic amounts and payout multipliers', () => {
|
|
const env = productionEnv({
|
|
DIRECT_GAME_PRICE: '0.01 XMR',
|
|
PAYOUT_MAX_PER_GAME: '-1',
|
|
DIRECT_PAYOUT_ESCAPE: 'double'
|
|
});
|
|
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config());
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('DIRECT_GAME_PRICE must be a non-negative integer'),
|
|
expect.stringContaining('PAYOUT_MAX_PER_GAME must be a non-negative integer'),
|
|
expect.stringContaining('DIRECT_PAYOUT_ESCAPE must be a finite number')
|
|
]));
|
|
});
|
|
|
|
test('rejects unknown payment modes instead of silently disabling them', () => {
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({ PAYMENT_MODES: 'direct,magic' }),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
|
|
expect(result.errors).toContain('PAYMENT_MODES must contain only direct and/or credits.');
|
|
});
|
|
|
|
test('rejects an invalid trusted-proxy hop count', () => {
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({ TRUST_PROXY_HOPS: 'all' }),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
|
|
expect(result.errors).toContain('TRUST_PROXY_HOPS must be an integer from 1 through 8.');
|
|
});
|
|
|
|
test('mainnet payout dispatch needs a deliberate acknowledgement', () => {
|
|
const payoutConfig = config({
|
|
payouts: {
|
|
enabled: true,
|
|
rules: { direct: { enabled: true }, credits: { enabled: true } }
|
|
}
|
|
});
|
|
const env = productionEnv({
|
|
MONERO_NETWORK: 'mainnet',
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUTS_ENABLED: 'true',
|
|
CREDITS_PAYOUTS_ENABLED: 'true'
|
|
});
|
|
const validator = new EnvironmentValidator({ env, logger: silentLogger });
|
|
|
|
expect(validator.validate(payoutConfig).errors)
|
|
.toContain('Mainnet payouts require the explicit safety acknowledgement ALLOW_MAINNET_PAYOUTS=true.');
|
|
|
|
env.ALLOW_MAINNET_PAYOUTS = 'true';
|
|
expect(validator.validate(payoutConfig).errors).toEqual([]);
|
|
});
|
|
|
|
test('stagenet 2x/3x payouts do not require the mainnet acknowledgement', () => {
|
|
const payoutConfig = config({
|
|
payouts: {
|
|
enabled: true,
|
|
rules: { direct: { enabled: true }, credits: { enabled: true } }
|
|
}
|
|
});
|
|
const env = productionEnv({
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUTS_ENABLED: 'true',
|
|
CREDITS_PAYOUTS_ENABLED: 'true'
|
|
});
|
|
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(payoutConfig);
|
|
expect(result.errors).toEqual([]);
|
|
expect(result.money.payoutsEnabled).toBe(true);
|
|
});
|
|
|
|
test('rejects direct 3x above cap and credits 2x below minimum', () => {
|
|
const env = productionEnv({
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUTS_ENABLED: 'true',
|
|
CREDITS_PAYOUTS_ENABLED: 'true'
|
|
});
|
|
const payoutConfig = config({
|
|
modes: { direct: { enabled: true, price: 20000n } },
|
|
payouts: {
|
|
enabled: true,
|
|
rules: {
|
|
direct: { enabled: true, maxPayout: 50000n },
|
|
credits: { enabled: true, baseValue: 1000n, minPayout: 3000n }
|
|
}
|
|
}
|
|
});
|
|
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(payoutConfig);
|
|
|
|
expect(result.errors).toEqual(expect.arrayContaining([
|
|
'Direct escapeWithTreasure payout (60000 atomic units) exceeds its configured maximum (50000).',
|
|
'Credits escape payout (2000 atomic units) is below its configured minimum (3000).'
|
|
]));
|
|
});
|
|
|
|
test('payout launch gate requires a reserve that covers the largest accepted liability', () => {
|
|
const payoutConfig = config({
|
|
payouts: {
|
|
enabled: true,
|
|
rules: { direct: { enabled: true }, credits: { enabled: false } }
|
|
}
|
|
});
|
|
const missing = productionEnv({
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUTS_ENABLED: 'true',
|
|
PAYOUT_MAX_PER_GAME: '60000',
|
|
BALANCE_CRITICAL: '',
|
|
BALANCE_WARN: ''
|
|
});
|
|
expect(new EnvironmentValidator({ env: missing, logger: silentLogger })
|
|
.validate(payoutConfig).errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('BALANCE_CRITICAL must be an explicit positive'),
|
|
expect.stringContaining('BALANCE_WARN must be an explicit positive')
|
|
]));
|
|
|
|
const underfunded = productionEnv({
|
|
PAYOUTS_ENABLED: 'true',
|
|
DIRECT_PAYOUTS_ENABLED: 'true',
|
|
PAYOUT_MAX_PER_GAME: '60000',
|
|
BALANCE_CRITICAL: '59999',
|
|
BALANCE_WARN: '59998'
|
|
});
|
|
expect(new EnvironmentValidator({ env: underfunded, logger: silentLogger })
|
|
.validate(payoutConfig).errors).toEqual(expect.arrayContaining([
|
|
expect.stringContaining('BALANCE_CRITICAL must be at least PAYOUT_MAX_PER_GAME (60000'),
|
|
expect.stringContaining('BALANCE_WARN must be greater than or equal to BALANCE_CRITICAL')
|
|
]));
|
|
});
|
|
|
|
test.each([
|
|
['BLOCK_SOURCE', 'simulated'],
|
|
['SIMULATED_BLOCKS', 'true'],
|
|
['FORCE_SIMULATED_BLOCKS', 'true']
|
|
])('rejects production simulation via %s', (key, value) => {
|
|
const result = new EnvironmentValidator({
|
|
env: productionEnv({ [key]: value }),
|
|
logger: silentLogger
|
|
}).validate(config());
|
|
|
|
expect(result.errors).toContain('Simulated blocks are forbidden when NODE_ENV=production.');
|
|
});
|
|
|
|
test('assertValid throws a typed error for startup to fail closed', () => {
|
|
const validator = new EnvironmentValidator({
|
|
env: productionEnv({ ADMIN_API_KEY: 'weak' }),
|
|
logger: silentLogger
|
|
});
|
|
|
|
expect(() => validator.assertValid(config())).toThrow(expect.objectContaining({
|
|
name: 'EnvironmentValidationError',
|
|
code: 'INVALID_ENVIRONMENT'
|
|
}));
|
|
});
|
|
});
|
|
|
|
describe('EnvironmentValidator development ergonomics', () => {
|
|
test('missing wallet/daemon endpoints warn without hard-stopping development', () => {
|
|
const env = { NODE_ENV: 'development', CRYPTO_TYPE: 'XMR', MONERO_NETWORK: 'stagenet' };
|
|
const result = new EnvironmentValidator({ env, logger: silentLogger }).validate(config());
|
|
|
|
expect(result.errors).toEqual([]);
|
|
expect(result.warnings.some(message => message.includes('PRIMARY_WALLET_ENDPOINT'))).toBe(true);
|
|
expect(result.warnings.some(message => message.includes('PRIMARY_RPC_ENDPOINT'))).toBe(true);
|
|
});
|
|
});
|