Observer integration step

This commit is contained in:
2026-10-23 10:20:58 -04:00
parent bd3d36a7e1
commit 216a419c0b
7 changed files with 161 additions and 45 deletions
+20 -14
View File
@@ -192,30 +192,36 @@ test('official wallet not-yet-created address-index error is recoverable but unr
await assert.rejects(createMoneroDepositAddressRpc(config, unrelated).getSubaddress(0, 1), /method failed \(-15\)/);
});
test('mTLS gateway allowlists only bounded observation and one-subaddress allocation methods', () => {
test('mTLS gateway enforces bounded per-certificate observer and address-allocator methods', () => {
const request = (method: string, params: Record<string, unknown> = {}) =>
({ jsonrpc: '2.0', id: 'fixture-id', method, params });
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_height')));
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0] })));
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('validate_address', { address: 'x'.repeat(95) })));
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'fixture', count: 1 })));
const observer = ['observer'] as const, allocator = ['address-allocator'] as const;
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_height'), observer));
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0] }), observer));
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'fixture', count: 1 }), allocator));
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('validate_address', { address: 'x'.repeat(95) }), allocator));
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_transfers', {
in: true, filter_by_height: true, min_height: 0, max_height: 100,
})));
}), observer));
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('incoming_transfers', {
transfer_type: 'all', account_index: 0, subaddr_indices: [2, 3],
})));
}), observer));
assert.throws(() => validateMoneroRpcGatewayRequest(request('incoming_transfers', {
transfer_type: 'all', account_index: 0, subaddr_indices: [2], key_image: true,
})));
}), observer));
assert.throws(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'fixture', count: 1 }), observer), /not allowed for this client certificate/);
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_transfers', {
in: true, filter_by_height: true, min_height: 0, max_height: 100,
}), allocator), /not allowed for this client certificate/);
for (const method of ['transfer', 'transfer_split', 'sweep_all', 'relay_tx', 'get_tx_key', 'query_key', 'open_wallet', 'get_balance']) {
assert.throws(() => validateMoneroRpcGatewayRequest(request(method)), /not allow-listed/);
assert.throws(() => validateMoneroRpcGatewayRequest(request(method), ['observer', 'address-allocator']), /not allowed for this client certificate/);
}
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0], all: true })));
assert.throws(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'x', count: 2 })));
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0], all: true }), observer));
assert.throws(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'x', count: 2 }), allocator));
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_transfers', {
in: true, filter_by_height: true, min_height: 0, max_height: 100_001,
})), /range/);
}), observer), /range/);
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_height'), []));
});
test('production mTLS transport config is mandatory and requires a bare HTTPS RPC endpoint', async () => {
@@ -223,10 +229,10 @@ test('production mTLS transport config is mandatory and requires a bare HTTPS RP
await assert.rejects(loadMoneroRpcGatewayConfig(''), /protected Monero RPC gateway configuration/);
await assert.rejects(createMoneroRpcGateway({ listenHost: '203.0.113.10', listenPort: 18085,
walletRpcUrl: 'http://127.0.0.1:18083/json_rpc', server: { certificateFile: 'missing', privateKeyFile: 'missing',
clientCaFile: 'missing' }, clientFingerprints: ['a'.repeat(64)] }), /private interface IP/);
clientCaFile: 'missing' }, clientPolicies: [{ fingerprint: 'a'.repeat(64), roles: ['observer'] }] }), /private interface IP/);
await assert.rejects(createMoneroRpcGateway({ listenHost: '127.0.0.1', listenPort: 18085,
walletRpcUrl: 'http://wallet.example/json_rpc', server: { certificateFile: 'missing', privateKeyFile: 'missing',
clientCaFile: 'missing' }, clientFingerprints: ['a'.repeat(64)] }), /loopback-only/);
clientCaFile: 'missing' }, clientPolicies: [{ fingerprint: 'a'.repeat(64), roles: ['observer'] }] }), /loopback-only/);
});
test('observer refuses non-loopback plaintext, URL credentials and malformed height ranges', async () => {